mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Compare commits
1227 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
95cb8c4b2c | ||
|
|
2712555c72 | ||
|
|
72066395d6 | ||
|
|
7b3e5f37b9 | ||
|
|
870a468584 | ||
|
|
0340cba441 | ||
|
|
91f845cbbf | ||
|
|
d0f21eafd1 | ||
|
|
36c2181a7f | ||
|
|
218e751333 | ||
|
|
995d2c5e4e | ||
|
|
0d7ae74f0f | ||
|
|
e6eea67c4b | ||
|
|
ea3e0ca34a | ||
|
|
3fedd8eb43 | ||
|
|
30408af719 | ||
|
|
910a400b18 | ||
|
|
f4ac18c3e1 | ||
|
|
cb06e1aaf7 | ||
|
|
d881e47ec9 | ||
|
|
dbafefb940 | ||
|
|
ee6589991d | ||
|
|
24578dcf88 | ||
|
|
ddae1a12d8 | ||
|
|
1c9b6c3eef | ||
|
|
158ecd4ab1 | ||
|
|
b6bcdef712 | ||
|
|
6eefd3f182 | ||
|
|
9280504f70 | ||
|
|
a6a671f687 | ||
|
|
4e3e8b7cc4 | ||
|
|
2b8fc3900a | ||
|
|
cff731dec7 | ||
|
|
c9c0c35964 | ||
|
|
afed5c65f5 | ||
|
|
0724c38582 | ||
|
|
31539a27ac | ||
|
|
201900aa0e | ||
|
|
3dcbe34485 | ||
|
|
31842cc0f2 | ||
|
|
f85349bd36 | ||
|
|
a18c24e19f | ||
|
|
e23c1b3872 | ||
|
|
3c2ef43526 | ||
|
|
c81aae0c6a | ||
|
|
ff1a9e8a52 | ||
|
|
90bfa47046 | ||
|
|
6383b8b46f | ||
|
|
76dc294748 | ||
|
|
0bc1dd96ed | ||
|
|
73d2d3cbbc | ||
|
|
d5fdd6881c | ||
|
|
b1bc25ba04 | ||
|
|
2c1204c247 | ||
|
|
05a1fd8e8d | ||
|
|
97f21394a7 | ||
|
|
c57a445046 | ||
|
|
fb981f1548 | ||
|
|
407dde2703 | ||
|
|
f175139fd3 | ||
|
|
bebd7eebe5 | ||
|
|
e01f6b42b1 | ||
|
|
bd1c6c076e | ||
|
|
46b1bd2fd2 | ||
|
|
2d7c5dcec7 | ||
|
|
93bcf6cef9 | ||
|
|
4b6a03ef56 | ||
|
|
7ff4230e13 | ||
|
|
7d7967ec00 | ||
|
|
b0883f034b | ||
|
|
f88628c469 | ||
|
|
ac7a964edf | ||
|
|
7bb22419df | ||
|
|
c443bc78d3 | ||
|
|
a5519537c5 | ||
|
|
ef67d1f33b | ||
|
|
31bd64f821 | ||
|
|
4c408ac7b1 | ||
|
|
30a9c1208a | ||
|
|
96fd824a3e | ||
|
|
0a469a380b | ||
|
|
5e3f31de30 | ||
|
|
af9fb8694e | ||
|
|
ebfdfc0c9f | ||
|
|
c9d6bb350d | ||
|
|
f980516462 | ||
|
|
795892f7c9 | ||
|
|
9576581d89 | ||
|
|
296223130e | ||
|
|
87e2da0e01 | ||
|
|
9156ea1114 | ||
|
|
b8fc2fd59e | ||
|
|
ef9723ed44 | ||
|
|
5105b503ea | ||
|
|
9bcb34e7d1 | ||
|
|
185a17edce | ||
|
|
f8fa7f4cf2 | ||
|
|
bcbb531414 | ||
|
|
2deab05c45 | ||
|
|
c7923300c6 | ||
|
|
c4651025be | ||
|
|
2897f88c8b | ||
|
|
ee20a6980b | ||
|
|
721c446fa8 | ||
|
|
7c43ee7208 | ||
|
|
c6ea37e081 | ||
|
|
171bf24133 | ||
|
|
3242f00812 | ||
|
|
15500b6535 | ||
|
|
700c880b92 | ||
|
|
7f2c07c918 | ||
|
|
0745d21761 | ||
|
|
50aa9b4dcb | ||
|
|
fe50cb845e | ||
|
|
f4b9807285 | ||
|
|
f534beb624 | ||
|
|
719cfaa906 | ||
|
|
98d9a6882b | ||
|
|
dbbeaf67b6 | ||
|
|
2a542bb8b9 | ||
|
|
29b630f6bf | ||
|
|
3aa00076c6 | ||
|
|
3cf99961d3 | ||
|
|
742727d6f2 | ||
|
|
5d99eee89a | ||
|
|
b52be27a99 | ||
|
|
e68f177e4a | ||
|
|
e0a6626650 | ||
|
|
1d8888bb14 | ||
|
|
d0958b7936 | ||
|
|
d9d316a627 | ||
|
|
0270910b74 | ||
|
|
432e0642ca | ||
|
|
c7ab8e4601 | ||
|
|
1a46f8a643 | ||
|
|
04145bde74 | ||
|
|
483e4c8f38 | ||
|
|
fe4468d49d | ||
|
|
a4e9ef989d | ||
|
|
4ed8550f1d | ||
|
|
4bec71e7c4 | ||
|
|
961fd94dd6 | ||
|
|
7915bbfa47 | ||
|
|
49e70e8e9b | ||
|
|
6063b02113 | ||
|
|
faa5a11c94 | ||
|
|
acab50cdcd | ||
|
|
4d7d897e06 | ||
|
|
af532e7fc9 | ||
|
|
fd2b383dbc | ||
|
|
98b33e184e | ||
|
|
3bc7c7473a | ||
|
|
4f6981b869 | ||
|
|
258d46a253 | ||
|
|
40a3eb5d4f | ||
|
|
35965235f3 | ||
|
|
368b24ea3b | ||
|
|
107b5ba36c | ||
|
|
84978a3d5d | ||
|
|
dd3b980c36 | ||
|
|
b9a553abf2 | ||
|
|
48b3dbc353 | ||
|
|
fb40d991bb | ||
|
|
5fe6a7196a | ||
|
|
f06b859c82 | ||
|
|
80dd59928e | ||
|
|
f22f76464a | ||
|
|
dfe1f16495 | ||
|
|
90a9030ecb | ||
|
|
4ab33a373c | ||
|
|
10e751d6e1 | ||
|
|
5f40669af7 | ||
|
|
97b4ed48db | ||
|
|
9fc096569a | ||
|
|
41636f7152 | ||
|
|
fc5d48de6f | ||
|
|
2a7f14a4ed | ||
|
|
1cb6778502 | ||
|
|
7f254e763d | ||
|
|
e0ff42b6a4 | ||
|
|
88fc8f5017 | ||
|
|
d7072928de | ||
|
|
0cc9ddba05 | ||
|
|
e3799cd0a8 | ||
|
|
ea6b30326e | ||
|
|
38768885e2 | ||
|
|
c360177c31 | ||
|
|
e88ea0bac1 | ||
|
|
a66b36c59c | ||
|
|
fb3b674b65 | ||
|
|
eff0e201f3 | ||
|
|
58d10cbba4 | ||
|
|
29bce8a9bc | ||
|
|
67dcf69a78 | ||
|
|
7ad8a04bda | ||
|
|
24e75603c6 | ||
|
|
545342dfb4 | ||
|
|
b15d6308bd | ||
|
|
ff0c381437 | ||
|
|
555260e954 | ||
|
|
8d584d1c48 | ||
|
|
967cf0cdfa | ||
|
|
41ba8455a0 | ||
|
|
d84f3bf887 | ||
|
|
153815d9e3 | ||
|
|
0250579445 | ||
|
|
7590d786b5 | ||
|
|
55809b7dc3 | ||
|
|
06026b0a09 | ||
|
|
2cd64aa650 | ||
|
|
0b980f6ab5 | ||
|
|
73dcef9fd1 | ||
|
|
4e3491ec64 | ||
|
|
176e440195 | ||
|
|
300545b289 | ||
|
|
057a9efb1f | ||
|
|
9edeff9aab | ||
|
|
f938506a3f | ||
|
|
4fb8c5a61b | ||
|
|
7a99c4a11a | ||
|
|
6c13504644 | ||
|
|
3ddf4ed4b2 | ||
|
|
6b59ff60ad | ||
|
|
7c64d689fd | ||
|
|
4ad9cdedb6 | ||
|
|
276cb8aecb | ||
|
|
e7ed82699c | ||
|
|
68cd3673af | ||
|
|
5f96570c07 | ||
|
|
5147ee8aee | ||
|
|
3cc2981b72 | ||
|
|
8038298485 | ||
|
|
d1a1bd5751 | ||
|
|
63b7aef91e | ||
|
|
8a51a47156 | ||
|
|
827efe071e | ||
|
|
56784b684a | ||
|
|
1fa52f043c | ||
|
|
ce44cfe877 | ||
|
|
8ba0a9f360 | ||
|
|
41dabac75b | ||
|
|
064708f088 | ||
|
|
66dc394215 | ||
|
|
5ca60699ef | ||
|
|
61d9ad1875 | ||
|
|
af6bbebac4 | ||
|
|
6f4fc4f2ca | ||
|
|
00f5001385 | ||
|
|
3f3cdca94f | ||
|
|
08cf01a5c6 | ||
|
|
df7f40dbc9 | ||
|
|
12c1725260 | ||
|
|
aa8380eb03 | ||
|
|
0e26f8fce1 | ||
|
|
8de14dcbce | ||
|
|
6871810c7c | ||
|
|
6ac294920e | ||
|
|
4b32eb397e | ||
|
|
b25297e8b9 | ||
|
|
ff90a2c3e6 | ||
|
|
3cbe8d1537 | ||
|
|
9293b622a3 | ||
|
|
6337e1cf7d | ||
|
|
3ff3fb29b0 | ||
|
|
149d297193 | ||
|
|
47f7b8870c | ||
|
|
7ce7d37da7 | ||
|
|
b422c754d6 | ||
|
|
be6e9019dc | ||
|
|
011bb86b5f | ||
|
|
8e1163d3db | ||
|
|
83c8b1c8e6 | ||
|
|
d44aecea90 | ||
|
|
c9288ab25b | ||
|
|
e39a2cb944 | ||
|
|
fb25edb51c | ||
|
|
06ff0498d4 | ||
|
|
63b27e7054 | ||
|
|
0260a12663 | ||
|
|
a05fcba6d9 | ||
|
|
5be5363f25 | ||
|
|
cc961b4eeb | ||
|
|
7e7795196c | ||
|
|
fd0fe1110d | ||
|
|
ea64fa0f85 | ||
|
|
2972f1a4d7 | ||
|
|
6990b398c2 | ||
|
|
a25b53bd44 | ||
|
|
871c503bc9 | ||
|
|
b7992ae9e1 | ||
|
|
0a8eb2fbb7 | ||
|
|
7863fb7632 | ||
|
|
6481d83b0c | ||
|
|
402d9b14ec | ||
|
|
feeb4f0ac0 | ||
|
|
143eb3213e | ||
|
|
cd96fc1daa | ||
|
|
73d9cd2e68 | ||
|
|
3faf817148 | ||
|
|
96c88b7472 | ||
|
|
e2795c7ef3 | ||
|
|
e7f6f7f3db | ||
|
|
dd9868c918 | ||
|
|
c2d3afae59 | ||
|
|
04a516d56b | ||
|
|
b27709e96f | ||
|
|
ebb7d23a05 | ||
|
|
97d121244f | ||
|
|
60dbc8ed84 | ||
|
|
29cd321724 | ||
|
|
467b834955 | ||
|
|
3bfa5d3dc4 | ||
|
|
f501f08e17 | ||
|
|
7d796369d6 | ||
|
|
c572011578 | ||
|
|
dbc1e7746b | ||
|
|
9325a1968b | ||
|
|
85166635b4 | ||
|
|
ed69a86529 | ||
|
|
ab2c77695b | ||
|
|
6ad81e1228 | ||
|
|
ff340c2449 | ||
|
|
f2b0f8ca9e | ||
|
|
0c213294ad | ||
|
|
7cdb7319ee | ||
|
|
d1ff8d6e3e | ||
|
|
d9ddc74d73 | ||
|
|
603df6d0f6 | ||
|
|
cbf847a57f | ||
|
|
e8fb676a24 | ||
|
|
273f3043fb | ||
|
|
8f165b05c7 | ||
|
|
8eb092a8d6 | ||
|
|
8dd1850905 | ||
|
|
0fa6bd56e2 | ||
|
|
c4c581525a | ||
|
|
d6a3824690 | ||
|
|
7dba141073 | ||
|
|
f2f6e639dc | ||
|
|
78afc5e4d5 | ||
|
|
b2d85b6c78 | ||
|
|
f3b0d37c54 | ||
|
|
6768ad2028 | ||
|
|
81cde4756a | ||
|
|
778564a954 | ||
|
|
1e4c8c3837 | ||
|
|
22408103ea | ||
|
|
383d3954d8 | ||
|
|
77a4e4aa50 | ||
|
|
19dedfa070 | ||
|
|
52696b014f | ||
|
|
8fb6de85f1 | ||
|
|
8270eb71db | ||
|
|
edcf12de5a | ||
|
|
da3399468c | ||
|
|
923c61fceb | ||
|
|
d3d939889e | ||
|
|
2b4d3fac6c | ||
|
|
1f67692727 | ||
|
|
bacbdb848b | ||
|
|
51ba7fc4cf | ||
|
|
59efed9e42 | ||
|
|
193fea4b69 | ||
|
|
b5cc2ff4c3 | ||
|
|
252ec58065 | ||
|
|
b8979ed8b0 | ||
|
|
c24f953e52 | ||
|
|
d139ca2706 | ||
|
|
bd421e877a | ||
|
|
25e1250710 | ||
|
|
fad1dad76e | ||
|
|
b3bd42b8f6 | ||
|
|
5aea538f09 | ||
|
|
16755e3140 | ||
|
|
cbfb7230a8 | ||
|
|
e438389953 | ||
|
|
6534ccb07e | ||
|
|
ca782dfc69 | ||
|
|
f1a662ba8a | ||
|
|
33fb2a38fc | ||
|
|
8b22e057bf | ||
|
|
68238e4e88 | ||
|
|
80fab6df99 | ||
|
|
6efe5e4ccc | ||
|
|
4c36d27d17 | ||
|
|
cc8dd59dbc | ||
|
|
7e91f57a42 | ||
|
|
f4c8bfa48c | ||
|
|
e0a14fdea6 | ||
|
|
b4af3586dc | ||
|
|
28722d385b | ||
|
|
dab6b9663a | ||
|
|
4e08334a3a | ||
|
|
dcb261a631 | ||
|
|
258fe34101 | ||
|
|
04917c45b6 | ||
|
|
08934d3c3c | ||
|
|
192657fa69 | ||
|
|
9c40c56c6c | ||
|
|
c4d0e4bac9 | ||
|
|
a62b14c8e4 | ||
|
|
3ec800e5e8 | ||
|
|
f30da06aaf | ||
|
|
03e9d23fa8 | ||
|
|
7dae9496ba | ||
|
|
b621cac18d | ||
|
|
13a576e871 | ||
|
|
19551671bd | ||
|
|
ecf9b78753 | ||
|
|
22303a2efc | ||
|
|
60aa20ac55 | ||
|
|
11cda40a40 | ||
|
|
ebbc4289e1 | ||
|
|
ac64811b35 | ||
|
|
7408744100 | ||
|
|
326fae88af | ||
|
|
0be73efdc1 | ||
|
|
486027f153 | ||
|
|
e42fc9a033 | ||
|
|
fc52670b2d | ||
|
|
4217de15a3 | ||
|
|
2d42ebff13 | ||
|
|
e629a51901 | ||
|
|
91273c2192 | ||
|
|
f8fce02a3d | ||
|
|
94c5920c27 | ||
|
|
c2eb505bd9 | ||
|
|
930e36fa2b | ||
|
|
9022438397 | ||
|
|
ba9aed5395 | ||
|
|
c780a8d944 | ||
|
|
3ed15ffefe | ||
|
|
ededb107a3 | ||
|
|
aebf4f9d28 | ||
|
|
336d0e551b | ||
|
|
5da9532614 | ||
|
|
956c2eaaaa | ||
|
|
62e7c34eb5 | ||
|
|
15da62777e | ||
|
|
3f2db7a199 | ||
|
|
adc9905038 | ||
|
|
fa82e1146b | ||
|
|
33a61e3414 | ||
|
|
70eda16515 | ||
|
|
e2eb7d3fa9 | ||
|
|
8aa0461d83 | ||
|
|
0f2cf9c5bb | ||
|
|
2c1f7e181c | ||
|
|
3555a0bd85 | ||
|
|
d7ce58d810 | ||
|
|
b0e1f6fe32 | ||
|
|
a324224e2a | ||
|
|
ddcb3e6f61 | ||
|
|
f4259642b8 | ||
|
|
db60afd13b | ||
|
|
f176a2a04f | ||
|
|
82be3a52dc | ||
|
|
4b2b8f653a | ||
|
|
555e5ecfb8 | ||
|
|
c0ca74ce6a | ||
|
|
674498052f | ||
|
|
1399b73dc2 | ||
|
|
c51f3d9e66 | ||
|
|
766a8c267f | ||
|
|
948d663fbf | ||
|
|
66be5c43a6 | ||
|
|
e0b00513b9 | ||
|
|
218761c594 | ||
|
|
5e4d6d5a78 | ||
|
|
b06df383dd | ||
|
|
361e92c600 | ||
|
|
69d0a2f96e | ||
|
|
bf078b2ea0 | ||
|
|
b5fa8f0ee0 | ||
|
|
7fb230e56b | ||
|
|
a53ecc4fdc | ||
|
|
3955eaaa3c | ||
|
|
d1d090a12b | ||
|
|
cd1509749a | ||
|
|
dd5278f442 | ||
|
|
631f5d48df | ||
|
|
c981cf32b9 | ||
|
|
984cabfc03 | ||
|
|
8d28f8f0a9 | ||
|
|
472fccc788 | ||
|
|
b1e715b272 | ||
|
|
6931cf7a45 | ||
|
|
1ccfc3c042 | ||
|
|
dd1976f173 | ||
|
|
eceaf40430 | ||
|
|
e565786bbc | ||
|
|
37a8a0a7f9 | ||
|
|
6aa3bb2ff3 | ||
|
|
516ee26ba0 | ||
|
|
1224abcd1d | ||
|
|
81eda96690 | ||
|
|
af9eb3cc64 | ||
|
|
d9bba16eb6 | ||
|
|
58cd50f674 | ||
|
|
b2b9dbeb8d | ||
|
|
0469141fee | ||
|
|
b16c7345a8 | ||
|
|
b1940e9293 | ||
|
|
1f51f632c4 | ||
|
|
ba199ff11b | ||
|
|
79c0f7769a | ||
|
|
761ec0659f | ||
|
|
5e31cdb7ac | ||
|
|
468384d82a | ||
|
|
230a4c5585 | ||
|
|
9048d341c6 | ||
|
|
b0f60bb671 | ||
|
|
9c87ecd4ec | ||
|
|
5324e5fcc2 | ||
|
|
c02da607e7 | ||
|
|
281fad5970 | ||
|
|
95d8201bd9 | ||
|
|
04cb27f85e | ||
|
|
a765e07c10 | ||
|
|
ef5cca26ea | ||
|
|
d86517606b | ||
|
|
9dcf3b7eea | ||
|
|
689f4abfde | ||
|
|
5357b55ef4 | ||
|
|
a10bdef484 | ||
|
|
58f20dab60 | ||
|
|
1c7abc1b24 | ||
|
|
02e0fd1357 | ||
|
|
7b3ff2a6d3 | ||
|
|
001e368775 | ||
|
|
aebc670b62 | ||
|
|
81810fced5 | ||
|
|
416fc83b4d | ||
|
|
e594007af1 | ||
|
|
3aee0acebb | ||
|
|
78a6ce4237 | ||
|
|
414c12aae4 | ||
|
|
724f9a5211 | ||
|
|
1a9c6f8f80 | ||
|
|
b640ffea44 | ||
|
|
6cf40e327c | ||
|
|
5959b00611 | ||
|
|
ce6b371206 | ||
|
|
e83c02653c | ||
|
|
487be2efb4 | ||
|
|
91d491f5ec | ||
|
|
9ec57b1ef6 | ||
|
|
cc31b5b78e | ||
|
|
c056b6240e | ||
|
|
cb8164c27f | ||
|
|
2ca286d826 | ||
|
|
dd7a3a6c8a | ||
|
|
7aeca7ebd3 | ||
|
|
4e4fd7501c | ||
|
|
a68eeb4669 | ||
|
|
ebf7a3f177 | ||
|
|
72b15967cc | ||
|
|
f75b4582c8 | ||
|
|
5d823571df | ||
|
|
cca61d281c | ||
|
|
34c9bc9b69 | ||
|
|
5fa048d242 | ||
|
|
ca928fc136 | ||
|
|
9bf53ab0cd | ||
|
|
542cc7de6f | ||
|
|
77fde3cda9 | ||
|
|
1eb595823b | ||
|
|
6d2167428f | ||
|
|
3820b9838d | ||
|
|
20a99e3236 | ||
|
|
c81f3d9f5d | ||
|
|
526de33bdd | ||
|
|
ee992b7255 | ||
|
|
7d459a9115 | ||
|
|
9faf036e4d | ||
|
|
306f241a4a | ||
|
|
41d1a1806d | ||
|
|
a825a693c1 | ||
|
|
7423d94935 | ||
|
|
82f9a0f2af | ||
|
|
7da62bb135 | ||
|
|
f2e5259916 | ||
|
|
4f4a48eb98 | ||
|
|
9c3b647f07 | ||
|
|
7f2d72ee59 | ||
|
|
548b8d270b | ||
|
|
696746f06c | ||
|
|
6f11a20feb | ||
|
|
c28c2ed506 | ||
|
|
8fb7f544de | ||
|
|
a76d9dba21 | ||
|
|
f1b3f7d5dd | ||
|
|
fd9e19d052 | ||
|
|
56363c083e | ||
|
|
bbdfec127a | ||
|
|
eb1087bf8d | ||
|
|
796f68640c | ||
|
|
ad091217d6 | ||
|
|
40e06b96a9 | ||
|
|
3fd186e22c | ||
|
|
e8c7f57704 | ||
|
|
e6de33034b | ||
|
|
73b6b9219b | ||
|
|
d1d1b0806f | ||
|
|
5ee97966ee | ||
|
|
4819f329ae | ||
|
|
52aed8a3f5 | ||
|
|
86ef129c1b | ||
|
|
aff872eea0 | ||
|
|
5d0025b4a7 | ||
|
|
e625a4e542 | ||
|
|
72ff1df61e | ||
|
|
3776fbafd9 | ||
|
|
cd91aff3bb | ||
|
|
b530ced9ed | ||
|
|
22957e9d88 | ||
|
|
d76aecb34d | ||
|
|
4cbb402931 | ||
|
|
4da82b110c | ||
|
|
880096a193 | ||
|
|
06c0fb8594 | ||
|
|
8e85543c6e | ||
|
|
4fb72fdf89 | ||
|
|
cf2616ec2b | ||
|
|
238aae5ea9 | ||
|
|
a963d10381 | ||
|
|
eb90d6a31a | ||
|
|
b013b5f50f | ||
|
|
14db333a6f | ||
|
|
6804f713b2 | ||
|
|
90a2c3b89b | ||
|
|
3c94664ac3 | ||
|
|
1b61a78e6d | ||
|
|
1defe51fd7 | ||
|
|
4cbd778ca1 | ||
|
|
e59ee47624 | ||
|
|
8b42399423 | ||
|
|
e84f82f47b | ||
|
|
e4189711bd | ||
|
|
9bca911b0b | ||
|
|
eb498f2bcc | ||
|
|
3d13f596a0 | ||
|
|
28984427e1 | ||
|
|
ed92acc468 | ||
|
|
3b9c87abc7 | ||
|
|
00181875c1 | ||
|
|
26fb2b97fb | ||
|
|
44194a074c | ||
|
|
4edd84f6e2 | ||
|
|
0956c96046 | ||
|
|
3013688447 | ||
|
|
0302913c3f | ||
|
|
72768362a5 | ||
|
|
39fa65ea37 | ||
|
|
4554c9d0df | ||
|
|
9b9b295045 | ||
|
|
ece786131e | ||
|
|
dc65313313 | ||
|
|
cb41a1ed66 | ||
|
|
c566dea232 | ||
|
|
8f04d23b3f | ||
|
|
b8b1039ada | ||
|
|
a7aafa27d0 | ||
|
|
18a1e82dda | ||
|
|
e26587e807 | ||
|
|
c3778b6419 | ||
|
|
fb85277894 | ||
|
|
8ae3399d56 | ||
|
|
7272de90f0 | ||
|
|
98c1f05ed6 | ||
|
|
90439501ad | ||
|
|
1bb3833cbe | ||
|
|
d7d8140050 | ||
|
|
3c77552211 | ||
|
|
5e7d1d1f69 | ||
|
|
8f92f9ec38 | ||
|
|
af23f063f2 | ||
|
|
13a3ce23e7 | ||
|
|
d7ff73243c | ||
|
|
25dd0440f6 | ||
|
|
b5301c4556 | ||
|
|
a22f69e2c1 |
810 changed files with 47206 additions and 11651 deletions
49
.github/workflows/package.yaml
vendored
49
.github/workflows/package.yaml
vendored
|
|
@ -2,41 +2,58 @@ name: Package
|
||||||
|
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest]
|
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set up JDK 16.0.1
|
- name: Set up JDK 22.0.2
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'temurin'
|
||||||
java-version: '16.0.1'
|
java-version: '22.0.2'
|
||||||
- name: Show Build Versions
|
- name: Show Build Versions
|
||||||
run: ./gradlew -v
|
run: ./gradlew -v
|
||||||
- name: Cache Gradle packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.gradle/caches
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew jpackage
|
run: ./gradlew jpackage
|
||||||
- name: Package zip distribution
|
- name: Package zip distribution
|
||||||
if: ${{ runner.os == 'Windows' }}
|
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
|
||||||
run: ./gradlew packageZipDistribution
|
run: ./gradlew packageZipDistribution
|
||||||
- name: Package tar distribution
|
- name: Package tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew packageTarDistribution
|
run: ./gradlew packageTarDistribution
|
||||||
- name: Upload Artifacts
|
- name: Repackage deb distribution
|
||||||
uses: actions/upload-artifact@v2
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./repackage.sh
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Sparrow Build - ${{ runner.os }}
|
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/jpackage/*
|
||||||
|
!build/jpackage/Sparrow/
|
||||||
|
- name: Headless build with Gradle
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./gradlew -Djava.awt.headless=true clean jpackage
|
||||||
|
- name: Package headless tar distribution
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||||
|
- name: Repackage headless deb distribution
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: ./repackage.sh
|
||||||
|
- name: Upload Headless Artifact
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||||
path: |
|
path: |
|
||||||
build/jpackage/*
|
build/jpackage/*
|
||||||
!build/jpackage/Sparrow/
|
!build/jpackage/Sparrow/
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +1,6 @@
|
||||||
[submodule "drongo"]
|
[submodule "drongo"]
|
||||||
path = drongo
|
path = drongo
|
||||||
url = ../../sparrowwallet/drongo.git
|
url = ../../sparrowwallet/drongo.git
|
||||||
|
[submodule "lark"]
|
||||||
|
path = lark
|
||||||
|
url = ../../sparrowwallet/lark.git
|
||||||
|
|
|
||||||
36
README.md
36
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
|
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
|
||||||
|
|
||||||
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
|
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [GitHub](https://github.com/sparrowwallet/sparrow/releases).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -16,8 +16,8 @@ or for those without SSH credentials:
|
||||||
|
|
||||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||||
|
|
||||||
In order to build, Sparrow requires Java 16 to be installed.
|
In order to build, Sparrow requires Java 22 or higher to be installed.
|
||||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
The release binaries are built with [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||||
|
|
||||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||||
|
|
||||||
|
|
@ -28,13 +28,15 @@ The Sparrow binaries can be built from source using
|
||||||
|
|
||||||
`./gradlew jpackage`
|
`./gradlew jpackage`
|
||||||
|
|
||||||
|
Note that to build the Windows installer, you will need to install [WiX](https://github.com/wixtoolset/wix3/releases).
|
||||||
|
|
||||||
When updating to the latest HEAD
|
When updating to the latest HEAD
|
||||||
|
|
||||||
`git pull --recurse-submodules`
|
`git pull --recurse-submodules`
|
||||||
|
|
||||||
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
|
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
|
||||||
|
|
||||||
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
> Video documentation of your build process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
|
|
@ -42,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||||
|
|
||||||
`./sparrow`
|
`./sparrow`
|
||||||
|
|
||||||
Java 16 or higher must be installed.
|
Java 22 or higher must be installed.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|
@ -62,10 +64,12 @@ Usage: sparrow [options]
|
||||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||||
--network, -n
|
--network, -n
|
||||||
Network to use
|
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`
|
`export SPARROW_NETWORK=testnet`
|
||||||
|
|
||||||
|
|
@ -75,13 +79,13 @@ Note that if you are connecting to an Electrum server when using testnet, that s
|
||||||
|
|
||||||
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||||
|
|
||||||
Platform | Location
|
| Platform | Location |
|
||||||
-------- | --------
|
|----------| -------- |
|
||||||
OSX | ~/.sparrow
|
| OSX | ~/.sparrow |
|
||||||
Linux | ~/.sparrow
|
| Linux | ~/.sparrow |
|
||||||
Windows | %APPDATA%/Sparrow
|
| Windows | %APPDATA%/Sparrow |
|
||||||
|
|
||||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
|
|
@ -91,6 +95,12 @@ Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab abo
|
||||||
|
|
||||||
Sparrow is licensed under the Apache 2 software licence.
|
Sparrow is licensed under the Apache 2 software licence.
|
||||||
|
|
||||||
|
## GPG Key
|
||||||
|
|
||||||
|
The Sparrow release binaries here and on [sparrowwallet.com](https://sparrowwallet.com/download/) are signed using [craigraw's GPG key](https://keybase.io/craigraw):
|
||||||
|
Fingerprint: D4D0D3202FC06849A257B38DE94618334C674B40
|
||||||
|
64-bit: E946 1833 4C67 4B40
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
|
|
||||||

|

|
||||||
|
|
|
||||||
592
build.gradle
592
build.gradle
|
|
@ -1,35 +1,38 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'extra-java-module-info'
|
id 'org-openjfx-javafxplugin'
|
||||||
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
|
id 'org.beryx.jlink' version '3.1.3'
|
||||||
id 'org.beryx.jlink' version '2.24.0'
|
id 'org.gradlex.extra-java-module-info' version '1.13'
|
||||||
|
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
def sparrowVersion = '1.5.2'
|
|
||||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
def osName = os.getFamilyName()
|
def osName = os.getFamilyName()
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
osName = "osx"
|
osName = "osx"
|
||||||
}
|
}
|
||||||
|
def osArch = "x64"
|
||||||
|
def releaseArch = "x86_64"
|
||||||
|
if(System.getProperty("os.arch") == "aarch64") {
|
||||||
|
osArch = "aarch64"
|
||||||
|
releaseArch = "aarch64"
|
||||||
|
}
|
||||||
|
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||||
|
|
||||||
group "com.sparrowwallet"
|
group = 'com.sparrowwallet'
|
||||||
version "${sparrowVersion}"
|
version = '2.3.1'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
|
||||||
maven { url 'https://jitpack.io' }
|
|
||||||
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask) {
|
tasks.withType(AbstractArchiveTask).configureEach {
|
||||||
preserveFileTimestamps = false
|
useFileSystemPermissions()
|
||||||
reproducibleFileOrder = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = "16"
|
version = headless ? "18" : "23.0.2"
|
||||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,44 +42,46 @@ java {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||||
implementation(project(':drongo')) {
|
implementation(project(':drongo'))
|
||||||
exclude group: 'org.hamcrest'
|
implementation(project(':lark'))
|
||||||
exclude group: 'junit'
|
implementation('com.google.guava:guava:33.5.0-jre')
|
||||||
}
|
implementation('com.google.code.gson:gson:2.9.1')
|
||||||
implementation('com.google.guava:guava:28.2-jre')
|
implementation('com.h2database:h2:2.1.214')
|
||||||
implementation('com.google.code.gson:gson:2.8.6')
|
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||||
implementation('com.h2database:h2:1.4.201-SNAPSHOT')
|
|
||||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
|
||||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
||||||
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
|
exclude group: 'org.slf4j'
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
}
|
||||||
|
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
||||||
|
exclude group: 'org.slf4j'
|
||||||
|
}
|
||||||
|
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||||
|
implementation('org.fxmisc.richtext:richtextfx:0.11.6')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
implementation('com.google.zxing:javase:3.4.0') {
|
implementation('com.google.zxing:javase:3.4.0') {
|
||||||
exclude group: 'com.beust', module: 'jcommander'
|
exclude group: 'com.beust', module: 'jcommander'
|
||||||
}
|
}
|
||||||
implementation('com.beust:jcommander:1.81')
|
implementation('org.jcommander:jcommander:2.0')
|
||||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||||
}
|
}
|
||||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('com.sparrowwallet:hummingbird:1.6.2')
|
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||||
exclude group: 'com.google.android.tools', module: 'dx'
|
implementation('co.nstant.in:cbor:0.9')
|
||||||
|
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||||
|
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||||
|
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||||
|
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||||
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
}
|
}
|
||||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||||
}
|
}
|
||||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
|
|
||||||
exclude group: 'org.jetbrains.kotlin'
|
|
||||||
}
|
|
||||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
|
||||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
|
||||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||||
|
|
@ -88,16 +93,27 @@ dependencies {
|
||||||
}
|
}
|
||||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||||
|
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.19')
|
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||||
testImplementation('junit:junit:4.12')
|
implementation('com.sparrowwallet:tern:1.0.6')
|
||||||
}
|
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||||
|
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||||
application {
|
implementation('org.apache.commons:commons-lang3:3.19.0')
|
||||||
mainModule = 'com.sparrowwallet.sparrow'
|
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||||
|
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||||
|
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||||
|
implementation('net.coobird:thumbnailator:0.4.18')
|
||||||
|
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||||
|
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||||
|
implementation('com.jcraft:jzlib:1.1.3')
|
||||||
|
implementation('io.github.doblon8:jzbar:0.2.1')
|
||||||
|
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||||
|
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||||
|
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
compileJava {
|
||||||
|
|
@ -112,17 +128,28 @@ compileJava {
|
||||||
processResources {
|
processResources {
|
||||||
doLast {
|
doLast {
|
||||||
delete fileTree("$buildDir/resources/main/native").matching {
|
delete fileTree("$buildDir/resources/main/native").matching {
|
||||||
exclude "${osName}/**"
|
exclude "${osName}/${osArch}/**"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
useJUnitPlatform()
|
||||||
|
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
}
|
}
|
||||||
|
|
||||||
run {
|
application {
|
||||||
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
mainModule = 'com.sparrowwallet.sparrow'
|
||||||
|
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
|
"--enable-native-access=com.sparrowwallet.drongo",
|
||||||
|
"--enable-native-access=com.sun.jna",
|
||||||
|
"--enable-native-access=javafx.graphics",
|
||||||
|
"--enable-native-access=com.fazecast.jSerialComm",
|
||||||
|
"--enable-native-access=org.usb4java",
|
||||||
|
"--enable-native-access=io.github.doblon8.jzbar",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||||
|
|
@ -131,19 +158,20 @@ run {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.io=com.google.gson"]
|
"--add-opens=java.base/java.io=com.google.gson",
|
||||||
|
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||||
|
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||||
|
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
|
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
}
|
||||||
|
if(headless) {
|
||||||
|
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,17 +185,20 @@ jlink {
|
||||||
requires 'jdk.crypto.cryptoki'
|
requires 'jdk.crypto.cryptoki'
|
||||||
requires 'java.management'
|
requires 'java.management'
|
||||||
requires 'io.leangen.geantyref'
|
requires 'io.leangen.geantyref'
|
||||||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
|
||||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
|
||||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||||
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
|
|
||||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||||
launcher {
|
launcher {
|
||||||
name = 'sparrow'
|
name = 'sparrow'
|
||||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||||
|
"--enable-native-access=com.sun.jna",
|
||||||
|
"--enable-native-access=javafx.graphics",
|
||||||
|
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||||
|
"--enable-native-access=com.fazecast.jSerialComm",
|
||||||
|
"--enable-native-access=org.usb4java",
|
||||||
|
"--enable-native-access=io.github.doblon8.jzbar",
|
||||||
|
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||||
|
|
@ -176,42 +207,60 @@ jlink {
|
||||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
|
||||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||||
|
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||||
"--add-opens=java.base/java.io=com.google.gson",
|
"--add-opens=java.base/java.io=com.google.gson",
|
||||||
|
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
|
"--add-reads=com.sparrowwallet.merged.module=ch.qos.logback.classic",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=org.slf4j",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core"]
|
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
|
||||||
|
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
|
||||||
|
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||||
|
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
|
|
||||||
|
if(os.windows) {
|
||||||
|
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||||
|
}
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||||
}
|
}
|
||||||
|
if(headless) {
|
||||||
|
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addExtraDependencies("javafx")
|
addExtraDependencies("javafx")
|
||||||
jpackage {
|
jpackage {
|
||||||
imageName = "Sparrow"
|
imageName = "Sparrow"
|
||||||
installerName = "Sparrow"
|
installerName = "Sparrow"
|
||||||
appVersion = "${sparrowVersion}"
|
appVersion = "${version}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
imageOptions = []
|
||||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
|
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||||
installerType = "exe"
|
installerType = "msi"
|
||||||
}
|
}
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
|
if(headless) {
|
||||||
|
installerName = "sparrowserver"
|
||||||
|
installerOptions = ['--license-file', 'LICENSE']
|
||||||
|
} else {
|
||||||
|
installerName = "sparrowwallet"
|
||||||
|
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||||
|
}
|
||||||
|
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||||
}
|
}
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
|
@ -220,23 +269,82 @@ jlink {
|
||||||
installerType = "dmg"
|
installerType = "dmg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if(os.linux) {
|
||||||
|
jpackageImage {
|
||||||
task removeGroupWritePermission(type: Exec) {
|
dependsOn('prepareModulesDir', 'copyUdevRules')
|
||||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
task packageZipDistribution(type: Zip) {
|
|
||||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
|
||||||
from("$buildDir/jpackage/") {
|
|
||||||
include "Sparrow/**"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task packageTarDistribution(type: Tar) {
|
if(os.linux) {
|
||||||
|
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||||
|
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
||||||
|
} else {
|
||||||
|
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('addUserWritePermission', Exec) {
|
||||||
|
if(os.windows) {
|
||||||
|
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||||
|
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||||
|
} else {
|
||||||
|
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('copyUdevRules', Copy) {
|
||||||
|
from('lark/src/main/resources/udev')
|
||||||
|
into(layout.buildDirectory.dir('image/conf/udev'))
|
||||||
|
include('*')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('prepareResourceDir', Copy) {
|
||||||
|
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
|
||||||
|
into(layout.buildDirectory.dir('deploy/package'))
|
||||||
|
include('*')
|
||||||
|
eachFile { file ->
|
||||||
|
if(file.name.equals('control') || file.name.endsWith('.spec')) {
|
||||||
|
filter { line ->
|
||||||
|
if(line.contains('${size}')) {
|
||||||
|
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
|
||||||
|
}
|
||||||
|
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static def getDirectorySize(File directory) {
|
||||||
|
long size = 0
|
||||||
|
if(directory.isFile()) {
|
||||||
|
size = directory.length()
|
||||||
|
} else if(directory.isDirectory()) {
|
||||||
|
directory.eachFileRecurse { file ->
|
||||||
|
if(file.isFile()) {
|
||||||
|
size += file.length()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Long.toString(size/1024 as long)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('removeGroupWritePermission', Exec) {
|
||||||
|
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('packageZipDistribution', Zip) {
|
||||||
|
archiveFileName = "Sparrow-${version}.zip"
|
||||||
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
|
preserveFileTimestamps = os.macOsX
|
||||||
|
from("$buildDir/jpackage/") {
|
||||||
|
include "Sparrow/**"
|
||||||
|
include "Sparrow.app/**"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('packageTarDistribution', Tar) {
|
||||||
dependsOn removeGroupWritePermission
|
dependsOn removeGroupWritePermission
|
||||||
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
|
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
compression = Compression.GZIP
|
compression = Compression.GZIP
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -245,60 +353,11 @@ task packageTarDistribution(type: Tar) {
|
||||||
}
|
}
|
||||||
|
|
||||||
extraJavaModuleInfo {
|
extraJavaModuleInfo {
|
||||||
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
|
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
|
||||||
exports('com.fasterxml.jackson.core')
|
|
||||||
exports('com.fasterxml.jackson.core.async')
|
|
||||||
exports('com.fasterxml.jackson.core.base')
|
|
||||||
exports('com.fasterxml.jackson.core.exc')
|
|
||||||
exports('com.fasterxml.jackson.core.filter')
|
|
||||||
exports('com.fasterxml.jackson.core.format')
|
|
||||||
exports('com.fasterxml.jackson.core.io')
|
|
||||||
exports('com.fasterxml.jackson.core.json')
|
|
||||||
exports('com.fasterxml.jackson.core.json.async')
|
|
||||||
exports('com.fasterxml.jackson.core.sym')
|
|
||||||
exports('com.fasterxml.jackson.core.type')
|
|
||||||
exports('com.fasterxml.jackson.core.util')
|
|
||||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
|
||||||
}
|
|
||||||
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
|
|
||||||
requires('com.fasterxml.jackson.core')
|
|
||||||
exports('com.fasterxml.jackson.annotation')
|
|
||||||
}
|
|
||||||
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
|
|
||||||
requires('java.desktop')
|
|
||||||
requires('java.logging')
|
|
||||||
requires('com.fasterxml.jackson.annotation')
|
|
||||||
requires('com.fasterxml.jackson.core')
|
|
||||||
requires('java.sql')
|
|
||||||
requires('java.xml')
|
|
||||||
exports('com.fasterxml.jackson.databind')
|
|
||||||
exports('com.fasterxml.jackson.databind.annotation')
|
|
||||||
exports('com.fasterxml.jackson.databind.cfg')
|
|
||||||
exports('com.fasterxml.jackson.databind.deser')
|
|
||||||
exports('com.fasterxml.jackson.databind.deser.impl')
|
|
||||||
exports('com.fasterxml.jackson.databind.deser.std')
|
|
||||||
exports('com.fasterxml.jackson.databind.exc')
|
|
||||||
exports('com.fasterxml.jackson.databind.ext')
|
|
||||||
exports('com.fasterxml.jackson.databind.introspect')
|
|
||||||
exports('com.fasterxml.jackson.databind.json')
|
|
||||||
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
|
|
||||||
exports('com.fasterxml.jackson.databind.jsonschema')
|
|
||||||
exports('com.fasterxml.jackson.databind.jsontype')
|
|
||||||
exports('com.fasterxml.jackson.databind.jsontype.impl')
|
|
||||||
exports('com.fasterxml.jackson.databind.module')
|
|
||||||
exports('com.fasterxml.jackson.databind.node')
|
|
||||||
exports('com.fasterxml.jackson.databind.ser')
|
|
||||||
exports('com.fasterxml.jackson.databind.ser.impl')
|
|
||||||
exports('com.fasterxml.jackson.databind.ser.std')
|
|
||||||
exports('com.fasterxml.jackson.databind.type')
|
|
||||||
exports('com.fasterxml.jackson.databind.util')
|
|
||||||
uses('com.fasterxml.jackson.databind.Module')
|
|
||||||
}
|
|
||||||
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
|
|
||||||
exports('tornadofx.control')
|
exports('tornadofx.control')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
}
|
}
|
||||||
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
|
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
|
||||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||||
requires('com.fasterxml.jackson.core')
|
requires('com.fasterxml.jackson.core')
|
||||||
|
|
@ -306,7 +365,7 @@ extraJavaModuleInfo {
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
requires('org.jetbrains.annotations')
|
requires('org.jetbrains.annotations')
|
||||||
}
|
}
|
||||||
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
|
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
|
||||||
exports('com.github.arteam.simplejsonrpc.client')
|
exports('com.github.arteam.simplejsonrpc.client')
|
||||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||||
|
|
@ -314,90 +373,26 @@ extraJavaModuleInfo {
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
requires('simple.json.rpc.core')
|
requires('simple.json.rpc.core')
|
||||||
}
|
}
|
||||||
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
|
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
|
||||||
exports('com.github.arteam.simplejsonrpc.server')
|
exports('com.github.arteam.simplejsonrpc.server')
|
||||||
requires('simple.json.rpc.core')
|
requires('simple.json.rpc.core')
|
||||||
requires('com.google.common')
|
requires('com.google.common')
|
||||||
requires('org.slf4j')
|
requires('org.slf4j')
|
||||||
requires('com.fasterxml.jackson.databind')
|
requires('com.fasterxml.jackson.databind')
|
||||||
}
|
}
|
||||||
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
||||||
exports('org.bridj')
|
exports('org.openpnp.capture')
|
||||||
exports('org.bridj.cpp')
|
exports('org.openpnp.capture.library')
|
||||||
requires('java.logging')
|
|
||||||
}
|
|
||||||
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
|
||||||
exports('com.github.sarxos.webcam')
|
|
||||||
exports('com.github.sarxos.webcam.ds.buildin')
|
|
||||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
|
||||||
requires('java.desktop')
|
requires('java.desktop')
|
||||||
requires('com.nativelibs4java.bridj')
|
requires('com.sun.jna')
|
||||||
requires('org.slf4j')
|
|
||||||
}
|
}
|
||||||
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||||
exports('de.codecentric.centerdevice')
|
|
||||||
requires('javafx.base')
|
|
||||||
requires('javafx.controls')
|
|
||||||
requires('javafx.graphics')
|
|
||||||
}
|
|
||||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
|
||||||
exports('com.csvreader')
|
exports('com.csvreader')
|
||||||
}
|
}
|
||||||
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
|
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||||
exports('org.slf4j.bridge')
|
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||||
requires('java.logging')
|
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||||
requires('org.slf4j')
|
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||||
}
|
|
||||||
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
|
||||||
exports('org.zeromq')
|
|
||||||
}
|
|
||||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
|
||||||
exports('org.json.simple')
|
|
||||||
}
|
|
||||||
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
|
|
||||||
exports('ch.qos.logback.classic')
|
|
||||||
requires('org.slf4j')
|
|
||||||
requires('logback.core')
|
|
||||||
requires('java.xml')
|
|
||||||
requires('java.logging')
|
|
||||||
}
|
|
||||||
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
|
|
||||||
exports('mu')
|
|
||||||
requires('kotlin.stdlib')
|
|
||||||
requires('org.slf4j')
|
|
||||||
}
|
|
||||||
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
|
|
||||||
exports('com.google.common.util.concurrent.internal')
|
|
||||||
}
|
|
||||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
|
||||||
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
|
|
||||||
exports('com.google.common.eventbus')
|
|
||||||
exports('com.google.common.net')
|
|
||||||
exports('com.google.common.base')
|
|
||||||
exports('com.google.common.collect')
|
|
||||||
exports('com.google.common.io')
|
|
||||||
exports('com.google.common.primitives')
|
|
||||||
exports('com.google.common.math')
|
|
||||||
requires('failureaccess')
|
|
||||||
requires('java.logging')
|
|
||||||
}
|
|
||||||
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
|
||||||
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
|
|
||||||
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
|
||||||
exports('org.jdbi.v3.core')
|
|
||||||
exports('org.jdbi.v3.core.mapper')
|
|
||||||
exports('org.jdbi.v3.core.statement')
|
|
||||||
exports('org.jdbi.v3.core.result')
|
|
||||||
exports('org.jdbi.v3.core.h2')
|
|
||||||
exports('org.jdbi.v3.core.spi')
|
|
||||||
requires('io.leangen.geantyref')
|
|
||||||
requires('java.sql')
|
|
||||||
requires('org.slf4j')
|
|
||||||
}
|
|
||||||
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
|
||||||
exports('io.leangen.geantyref')
|
|
||||||
}
|
|
||||||
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
|
|
||||||
exports('org.fxmisc.richtext')
|
exports('org.fxmisc.richtext')
|
||||||
exports('org.fxmisc.richtext.event')
|
exports('org.fxmisc.richtext.event')
|
||||||
exports('org.fxmisc.richtext.model')
|
exports('org.fxmisc.richtext.model')
|
||||||
|
|
@ -406,23 +401,23 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.fxmisc.flowless')
|
requires('org.fxmisc.flowless')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
requires('org.fxmisc.undo.undofx')
|
requires('org.fxmisc.undo')
|
||||||
requires('org.fxmisc.wellbehaved')
|
requires('org.fxmisc.wellbehaved')
|
||||||
}
|
}
|
||||||
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
}
|
}
|
||||||
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
|
||||||
exports('org.fxmisc.flowless')
|
exports('org.fxmisc.flowless')
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('org.reactfx.reactfx')
|
requires('org.reactfx.reactfx')
|
||||||
}
|
}
|
||||||
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
|
||||||
exports('org.reactfx')
|
exports('org.reactfx')
|
||||||
exports('org.reactfx.value')
|
exports('org.reactfx.value')
|
||||||
exports('org.reactfx.collection')
|
exports('org.reactfx.collection')
|
||||||
|
|
@ -431,136 +426,57 @@ extraJavaModuleInfo {
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
requires('javafx.controls')
|
requires('javafx.controls')
|
||||||
}
|
}
|
||||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
|
||||||
|
exports('io.reactivex.rxjavafx.schedulers')
|
||||||
|
requires('io.reactivex.rxjava2')
|
||||||
|
requires('javafx.graphics')
|
||||||
|
}
|
||||||
|
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
|
||||||
|
exports('org.flywaydb.core')
|
||||||
|
exports('org.flywaydb.core.api')
|
||||||
|
exports('org.flywaydb.core.api.exception')
|
||||||
|
exports('org.flywaydb.core.api.configuration')
|
||||||
|
uses('org.flywaydb.core.extensibility.Plugin')
|
||||||
|
requires('java.sql')
|
||||||
|
}
|
||||||
|
module('org.fxmisc.wellbehaved:wellbehavedfx', 'org.fxmisc.wellbehaved') {
|
||||||
requires('javafx.base')
|
requires('javafx.base')
|
||||||
requires('javafx.graphics')
|
requires('javafx.graphics')
|
||||||
}
|
}
|
||||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
|
||||||
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
|
requires('java.desktop')
|
||||||
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
|
|
||||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
|
||||||
exports('kotlin')
|
|
||||||
}
|
}
|
||||||
module('hummingbird-1.6.2.jar', 'com.sparrowwallet.hummingbird', '1.6.2') {
|
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
|
||||||
exports('com.sparrowwallet.hummingbird')
|
|
||||||
exports('com.sparrowwallet.hummingbird.registry')
|
|
||||||
requires('co.nstant.in.cbor')
|
|
||||||
}
|
|
||||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
|
||||||
exports('co.nstant.in.cbor')
|
exports('co.nstant.in.cbor')
|
||||||
|
exports('co.nstant.in.cbor.model')
|
||||||
|
exports('co.nstant.in.cbor.builder')
|
||||||
}
|
}
|
||||||
module('nightjar-0.2.19.jar', 'com.sparrowwallet.nightjar', '0.2.19') {
|
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
|
||||||
requires('com.google.common')
|
|
||||||
requires('net.sourceforge.streamsupport')
|
|
||||||
requires('org.slf4j')
|
|
||||||
requires('org.bouncycastle.provider')
|
|
||||||
requires('com.fasterxml.jackson.databind')
|
|
||||||
requires('com.fasterxml.jackson.annotation')
|
|
||||||
requires('com.fasterxml.jackson.core')
|
|
||||||
requires('logback.classic')
|
|
||||||
requires('org.json')
|
|
||||||
exports('com.samourai.http.client')
|
|
||||||
exports('com.samourai.tor.client')
|
|
||||||
exports('com.samourai.wallet.api.backend')
|
|
||||||
exports('com.samourai.wallet.api.backend.beans')
|
|
||||||
exports('com.samourai.wallet.client.indexHandler')
|
|
||||||
exports('com.samourai.wallet.hd')
|
|
||||||
exports('com.samourai.wallet.util')
|
|
||||||
exports('com.samourai.whirlpool.client.event')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.beans')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
|
|
||||||
exports('com.samourai.whirlpool.client.whirlpool')
|
|
||||||
exports('com.samourai.whirlpool.client.whirlpool.beans')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.pool')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.utxo')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.supplier')
|
|
||||||
exports('com.samourai.whirlpool.client.mix.handler')
|
|
||||||
exports('com.samourai.whirlpool.client.mix.listener')
|
|
||||||
exports('com.samourai.whirlpool.protocol.beans')
|
|
||||||
exports('com.samourai.whirlpool.protocol.rest')
|
|
||||||
exports('com.samourai.whirlpool.client.tx0')
|
|
||||||
exports('com.samourai.wallet.segwit.bech32')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
|
||||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
|
||||||
exports('com.sparrowwallet.nightjar.http')
|
|
||||||
exports('com.sparrowwallet.nightjar.stomp')
|
|
||||||
exports('com.sparrowwallet.nightjar.tor')
|
|
||||||
}
|
|
||||||
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
|
|
||||||
exports('com.zeroleak.throwingsupplier')
|
|
||||||
}
|
|
||||||
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
|
|
||||||
exports('com.squareup.okhttp')
|
|
||||||
}
|
|
||||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
|
||||||
exports('okio')
|
|
||||||
}
|
|
||||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
|
||||||
exports('com.auth0.jwt')
|
|
||||||
}
|
|
||||||
module('json-20180130.jar', 'org.json', '1.0') {
|
|
||||||
exports('org.json')
|
|
||||||
}
|
|
||||||
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
|
|
||||||
exports('com.lambdaworks.codec')
|
|
||||||
exports('com.lambdaworks.crypto')
|
|
||||||
}
|
|
||||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
|
||||||
requires('jdk.unsupported')
|
requires('jdk.unsupported')
|
||||||
exports('java8.util')
|
exports('java8.util')
|
||||||
exports('java8.util.function')
|
exports('java8.util.function')
|
||||||
exports('java8.util.stream')
|
exports('java8.util.stream')
|
||||||
}
|
}
|
||||||
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
|
||||||
exports('com.google.protobuf')
|
exports('net.coobird.thumbnailator')
|
||||||
|
requires('java.desktop')
|
||||||
}
|
}
|
||||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||||
exports('org.apache.commons.text')
|
|
||||||
}
|
|
||||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
|
||||||
exports('net.jcip.annotations')
|
|
||||||
}
|
|
||||||
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
|
||||||
exports('org.berndpruenster.netlayer.tor')
|
|
||||||
requires('com.github.ravn.jsocks')
|
|
||||||
requires('com.github.JesusMcCloud.jtorctl')
|
|
||||||
requires('kotlin.stdlib')
|
|
||||||
requires('commons.compress')
|
|
||||||
requires('org.tukaani.xz')
|
|
||||||
requires('java.management')
|
|
||||||
requires('io.github.microutils.kotlin.logging')
|
|
||||||
}
|
|
||||||
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
|
|
||||||
exports('net.freehaven.tor.control')
|
|
||||||
}
|
|
||||||
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
|
|
||||||
exports('org.apache.commons.compress')
|
|
||||||
requires('org.tukaani.xz')
|
|
||||||
}
|
|
||||||
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
|
|
||||||
exports('org.tukaani.xz')
|
|
||||||
}
|
|
||||||
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
|
|
||||||
exports('com.runjva.sourceforge.jsocks.protocol')
|
|
||||||
requires('org.slf4j')
|
|
||||||
}
|
|
||||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
|
||||||
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
|
|
||||||
requires('java.xml')
|
|
||||||
}
|
|
||||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
|
||||||
exports('kotlin.jvm')
|
|
||||||
exports('kotlin.collections')
|
|
||||||
}
|
|
||||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
|
||||||
exports('com.beust.jcommander')
|
exports('com.beust.jcommander')
|
||||||
}
|
}
|
||||||
module('junit-4.12.jar', 'junit', '4.12') {
|
module('com.sparrowwallet:hid4java', 'org.hid4java') {
|
||||||
exports('org.junit')
|
requires('com.sun.jna')
|
||||||
|
exports('org.hid4java')
|
||||||
|
exports('org.hid4java.jna')
|
||||||
}
|
}
|
||||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
module('com.sparrowwallet:usb4java', 'org.usb4java') {
|
||||||
|
exports('org.usb4java')
|
||||||
|
}
|
||||||
|
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
||||||
|
exports('com.jcraft.jzlib')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kmpTorResourceFilterJar {
|
||||||
|
keepTorCompilation("current","current")
|
||||||
}
|
}
|
||||||
|
|
@ -3,19 +3,21 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.ow2.asm:asm:8.0.1'
|
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://plugins.gradle.org/m2/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gradlePlugin {
|
gradlePlugin {
|
||||||
plugins {
|
plugins {
|
||||||
// here we register our plugin with an ID
|
register("org-openjfx-javafxplugin") {
|
||||||
register("extra-java-module-info") {
|
id = "org-openjfx-javafxplugin"
|
||||||
id = "extra-java-module-info"
|
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package org.gradle.sample.transform.javamodules;
|
|
||||||
|
|
||||||
import org.gradle.api.Plugin;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.api.artifacts.Configuration;
|
|
||||||
import org.gradle.api.attributes.Attribute;
|
|
||||||
import org.gradle.api.plugins.JavaPlugin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry point of our plugin that should be applied in the root project.
|
|
||||||
*/
|
|
||||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(Project project) {
|
|
||||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
|
||||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
|
||||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
|
||||||
|
|
||||||
// setup the transform for all projects in the build
|
|
||||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
|
||||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
|
||||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
|
||||||
|
|
||||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
|
||||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
|
||||||
c -> c.getAttributes().attribute(javaModule, true));
|
|
||||||
|
|
||||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
|
||||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
|
||||||
|
|
||||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
|
||||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
|
||||||
t.parameters(p -> {
|
|
||||||
p.setModuleInfo(extension.getModuleInfo());
|
|
||||||
p.setAutomaticModules(extension.getAutomaticModules());
|
|
||||||
});
|
|
||||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
|
||||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
|
||||||
if (!configuration.isCanBeResolved()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
|
||||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
|
||||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package org.gradle.sample.transform.javamodules;
|
|
||||||
|
|
||||||
|
|
||||||
import org.gradle.api.Action;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class to collect all the module information we want to add.
|
|
||||||
* Here the class is used as extension that can be configured in the build script
|
|
||||||
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
|
||||||
*/
|
|
||||||
public class ExtraModuleInfoPluginExtension {
|
|
||||||
|
|
||||||
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
|
||||||
private final Map<String, String> automaticModules = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add full module information for a given Jar file.
|
|
||||||
*/
|
|
||||||
public void module(String jarName, String moduleName, String moduleVersion) {
|
|
||||||
module(jarName, moduleName, moduleVersion, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
|
||||||
*/
|
|
||||||
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
|
||||||
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
|
||||||
if (conf != null) {
|
|
||||||
conf.execute(moduleInfo);
|
|
||||||
}
|
|
||||||
this.moduleInfo.put(jarName, moduleInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add only an automatic module name to a given jar file.
|
|
||||||
*/
|
|
||||||
public void automaticModule(String jarName, String moduleName) {
|
|
||||||
automaticModules.put(jarName, moduleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, ModuleInfo> getModuleInfo() {
|
|
||||||
return moduleInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, String> getAutomaticModules() {
|
|
||||||
return automaticModules;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,174 +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")) {
|
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java
Normal file
114
buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018, 2020, Gluon
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the copyright holder nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package org.openjfx.gradle;
|
||||||
|
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public enum JavaFXModule {
|
||||||
|
|
||||||
|
BASE,
|
||||||
|
GRAPHICS(BASE),
|
||||||
|
CONTROLS(BASE, GRAPHICS),
|
||||||
|
FXML(BASE, GRAPHICS),
|
||||||
|
MEDIA(BASE, GRAPHICS),
|
||||||
|
SWING(BASE, GRAPHICS),
|
||||||
|
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
||||||
|
|
||||||
|
static final String PREFIX_MODULE = "javafx.";
|
||||||
|
private static final String PREFIX_ARTIFACT = "javafx-";
|
||||||
|
|
||||||
|
private List<JavaFXModule> dependentModules;
|
||||||
|
|
||||||
|
JavaFXModule(JavaFXModule...dependentModules) {
|
||||||
|
this.dependentModules = List.of(dependentModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
||||||
|
return Stream.of(JavaFXModule.values())
|
||||||
|
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModuleName() {
|
||||||
|
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModuleJarFileName() {
|
||||||
|
return getModuleName() + ".jar";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getArtifactName() {
|
||||||
|
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
||||||
|
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
||||||
|
return p.matcher(jarFileName).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
||||||
|
validateModules(moduleNames);
|
||||||
|
|
||||||
|
return moduleNames.stream()
|
||||||
|
.map(JavaFXModule::fromModuleName)
|
||||||
|
.flatMap(Optional::stream)
|
||||||
|
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateModules(List<String> moduleNames) {
|
||||||
|
var invalidModules = moduleNames.stream()
|
||||||
|
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (! invalidModules.isEmpty()) {
|
||||||
|
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JavaFXModule> getDependentModules() {
|
||||||
|
return dependentModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JavaFXModule> getMavenDependencies() {
|
||||||
|
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
||||||
|
dependencies.add(0, this);
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
}
|
||||||
164
buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java
Normal file
164
buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018, Gluon
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the copyright holder nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package org.openjfx.gradle;
|
||||||
|
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
||||||
|
|
||||||
|
public class JavaFXOptions {
|
||||||
|
|
||||||
|
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
||||||
|
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
||||||
|
|
||||||
|
private final Project project;
|
||||||
|
private final JavaFXPlatform platform;
|
||||||
|
|
||||||
|
private String version = "16";
|
||||||
|
private String sdk;
|
||||||
|
private String configuration = "implementation";
|
||||||
|
private String lastUpdatedConfiguration;
|
||||||
|
private List<String> modules = new ArrayList<>();
|
||||||
|
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
||||||
|
|
||||||
|
public JavaFXOptions(Project project) {
|
||||||
|
this.project = project;
|
||||||
|
this.platform = JavaFXPlatform.detect(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JavaFXPlatform getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(String version) {
|
||||||
|
this.version = version;
|
||||||
|
updateJavaFXDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, the JavaFX modules will be taken from this local
|
||||||
|
* repository, and not from Maven Central
|
||||||
|
* @param sdk, the path to the local JavaFX SDK folder
|
||||||
|
*/
|
||||||
|
public void setSdk(String sdk) {
|
||||||
|
this.sdk = sdk;
|
||||||
|
updateJavaFXDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSdk() {
|
||||||
|
return sdk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the configuration name for dependencies, e.g.
|
||||||
|
* 'implementation', 'compileOnly' etc.
|
||||||
|
* @param configuration The configuration name for dependencies
|
||||||
|
*/
|
||||||
|
public void setConfiguration(String configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
updateJavaFXDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfiguration() {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getModules() {
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModules(List<String> modules) {
|
||||||
|
this.modules = modules;
|
||||||
|
updateJavaFXDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void modules(String...moduleNames) {
|
||||||
|
setModules(List.of(moduleNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateJavaFXDependencies() {
|
||||||
|
clearJavaFXDependencies();
|
||||||
|
|
||||||
|
String configuration = getConfiguration();
|
||||||
|
JavaFXModule.getJavaFXModules(this.modules).stream()
|
||||||
|
.sorted()
|
||||||
|
.forEach(javaFXModule -> {
|
||||||
|
if (customSDKArtifactRepository != null) {
|
||||||
|
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
||||||
|
} else {
|
||||||
|
project.getDependencies().add(configuration,
|
||||||
|
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
||||||
|
getVersion(), getPlatform().getClassifier()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lastUpdatedConfiguration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearJavaFXDependencies() {
|
||||||
|
if (customSDKArtifactRepository != null) {
|
||||||
|
project.getRepositories().remove(customSDKArtifactRepository);
|
||||||
|
customSDKArtifactRepository = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdk != null && ! sdk.isEmpty()) {
|
||||||
|
Map<String, String> dirs = new HashMap<>();
|
||||||
|
dirs.put("name", "customSDKArtifactRepository");
|
||||||
|
if (sdk.endsWith(File.separator)) {
|
||||||
|
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
||||||
|
} else {
|
||||||
|
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
||||||
|
}
|
||||||
|
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastUpdatedConfiguration == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
||||||
|
if (configuration != null) {
|
||||||
|
if (customSDKArtifactRepository != null) {
|
||||||
|
configuration.getDependencies()
|
||||||
|
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
||||||
|
}
|
||||||
|
configuration.getDependencies()
|
||||||
|
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018, Gluon
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* * Neither the name of the copyright holder nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from
|
||||||
|
* this software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package org.openjfx.gradle;
|
||||||
|
|
||||||
|
import com.google.gradle.osdetector.OsDetector;
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public enum JavaFXPlatform {
|
||||||
|
|
||||||
|
LINUX("linux", "linux-x86_64"),
|
||||||
|
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
||||||
|
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
||||||
|
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
||||||
|
WINDOWS("win", "windows-x86_64"),
|
||||||
|
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
||||||
|
OSX("mac", "osx-x86_64"),
|
||||||
|
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
||||||
|
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
||||||
|
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
||||||
|
|
||||||
|
private final String classifier;
|
||||||
|
private final String osDetectorClassifier;
|
||||||
|
|
||||||
|
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
||||||
|
this.classifier = classifier;
|
||||||
|
this.osDetectorClassifier = osDetectorClassifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClassifier() {
|
||||||
|
return classifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JavaFXPlatform detect(Project project) {
|
||||||
|
|
||||||
|
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
||||||
|
|
||||||
|
if("true".equals(System.getProperty("java.awt.headless"))) {
|
||||||
|
osClassifier += "-monocle";
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( JavaFXPlatform platform: values()) {
|
||||||
|
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String supportedPlatforms = Arrays.stream(values())
|
||||||
|
.map(p->p.osDetectorClassifier)
|
||||||
|
.collect(Collectors.joining("', '", "'", "'"));
|
||||||
|
|
||||||
|
throw new GradleException(
|
||||||
|
String.format(
|
||||||
|
"Unsupported JavaFX platform found: '%s'! " +
|
||||||
|
"This plugin is designed to work on supported platforms only." +
|
||||||
|
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
47
buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java
Normal file
47
buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.openjfx.gradle.tasks.ExecTask;
|
||||||
|
|
||||||
|
public class JavaFXPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(Project project) {
|
||||||
|
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||||
|
|
||||||
|
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||||
|
|
||||||
|
project.getTasks().register("configJavafxRun", ExecTask.class, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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.plugins.ApplicationPlugin;
|
||||||
|
import org.gradle.api.tasks.JavaExec;
|
||||||
|
import org.gradle.api.tasks.TaskAction;
|
||||||
|
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.Arrays;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public class ExecTask extends DefaultTask {
|
||||||
|
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()) {
|
||||||
|
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()));
|
||||||
|
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||||
|
}
|
||||||
|
} 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,126 @@
|
||||||
## Reproducible builds
|
# Reproducible builds
|
||||||
|
|
||||||
Reproducibility is a goal of the Sparrow Wallet project.
|
Reproducibility is a goal of the Sparrow Wallet project.
|
||||||
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
||||||
|
|
||||||
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
||||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||||
Work on resolving both of these issues is ongoing.
|
Work on resolving both of these issues is ongoing.
|
||||||
|
|
||||||
### Reproducing a release
|
## Reproducing a release
|
||||||
|
|
||||||
#### Install Java
|
### Install Java
|
||||||
|
|
||||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||||
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
|
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
|
||||||
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
|
||||||
|
#### Java from Adoptium github repo
|
||||||
|
|
||||||
|
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||||
|
|
||||||
For reference, the downloads are as follows:
|
For reference, the downloads are as follows:
|
||||||
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
|
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
|
||||||
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
|
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
|
||||||
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
|
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
|
||||||
|
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
|
||||||
|
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.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:
|
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||||
```shell
|
|
||||||
sudo apt-get install -y wget apt-transport-https gnupg
|
- Install dependencies:
|
||||||
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
|
```sh
|
||||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
|
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
|
|
||||||
rm adoptopenjdk-keyring.gpg
|
|
||||||
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
|
|
||||||
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
|
|
||||||
sudo apt update -y
|
|
||||||
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Download Adoptium public PGP key:
|
||||||
|
```sh
|
||||||
|
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||||
|
```
|
||||||
|
|
||||||
|
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||||
|
```
|
||||||
|
gpg --import --import-options show-only adoptium.asc
|
||||||
|
```
|
||||||
|
If key doesn't match, do not proceed.
|
||||||
|
|
||||||
|
Add Adoptium PGP key to a the keyring shared folder:
|
||||||
|
```sh
|
||||||
|
sudo cp adoptium.asc /usr/share/keyrings/
|
||||||
|
```
|
||||||
|
|
||||||
|
Add Adoptium debian repository:
|
||||||
|
```sh
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||||
|
```
|
||||||
|
|
||||||
|
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||||
|
```
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt-get install -y temurin-22-jdk=22.0.2+9
|
||||||
|
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).
|
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||||
See the installation [instructions here](https://sdkman.io/install).
|
See the installation [instructions here](https://sdkman.io/install).
|
||||||
Once installed, run
|
Once installed, run
|
||||||
```shell
|
```shell
|
||||||
sdk install java 16.0.1.hs-adpt
|
sdk install java 22.0.2-tem
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Other requirements
|
### Other requirements
|
||||||
|
|
||||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||||
```shell
|
```shell
|
||||||
sudo apt install -y rpm fakeroot binutils
|
sudo apt install -y rpm fakeroot binutils
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Building the binaries
|
### Building the binaries
|
||||||
|
|
||||||
|
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||||
|
|
||||||
The project can cloned for a specific release tag as follows:
|
|
||||||
```shell
|
```shell
|
||||||
GIT_TAG="1.5.2"
|
GIT_TAG="2.3.0"
|
||||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Thereafter, building should be straightforward:
|
The project can then be initially cloned as follows:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd sparrow
|
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have the sparrow repo cloned, fetch all new updates and checkout the release. For this, change into your local sparrow folder and execute:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd {yourPathToSparrow}/sparrow
|
||||||
|
git pull --recurse-submodules
|
||||||
|
git checkout "${GIT_TAG}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
|
||||||
|
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
|
||||||
|
Only then your build will be comparable to the provided one in the release section of Github.
|
||||||
|
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git submodule update --checkout
|
||||||
|
```
|
||||||
|
|
||||||
|
Thereafter, building should be straightforward. If not already done, change into the sparrow folder and run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd {yourPathToSparrow}/sparrow # if you aren't already in the sparrow folder
|
||||||
./gradlew jpackage
|
./gradlew jpackage
|
||||||
```
|
```
|
||||||
|
|
||||||
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
||||||
|
|
||||||
#### Verifying the binaries are identical
|
### Verifying the binaries are identical
|
||||||
|
|
||||||
|
Verify the built binaries against the released binaries on https://github.com/sparrowwallet/sparrow/releases.
|
||||||
|
|
||||||
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
|
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
|
||||||
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
|
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
|
||||||
|
|
@ -76,3 +132,4 @@ diff -r build/jpackage/Sparrow /tmp/Sparrow
|
||||||
|
|
||||||
This command should have no output indicating that the two folders (and all their contents) are identical.
|
This command should have no output indicating that the two folders (and all their contents) are identical.
|
||||||
|
|
||||||
|
If there is output, please open an issue with detailed instructions to reproduce, including build system platform.
|
||||||
|
|
|
||||||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6c03c4106ae612c0b80370c3ed5495b1ba9225bc
|
Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
295
gradlew
vendored
295
gradlew
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,81 +15,114 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/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/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
# This is normally unused
|
||||||
APP_BASE_NAME=`basename "$0"`
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
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.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
# shellcheck disable=SC2039,SC3045
|
||||||
fi
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
ulimit -n $MAX_FD
|
warn "Could not query maximum file descriptor limit"
|
||||||
if [ $? -ne 0 ] ; then
|
esac
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
case $MAX_FD in #(
|
||||||
fi
|
'' | soft) :;; #(
|
||||||
else
|
*)
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
fi
|
# shellcheck disable=SC2039,SC3045
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# 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" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|
|
||||||
40
gradlew.bat
vendored
40
gradlew.bat
vendored
|
|
@ -13,8 +13,10 @@
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
|
|
@ -25,7 +27,8 @@
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
|
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@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
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
|
||||||
1
lark
Submodule
1
lark
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0
|
||||||
48
repackage.sh
Executable file
48
repackage.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Define paths
|
||||||
|
BUILD_DIR="build"
|
||||||
|
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||||
|
TEMP_DIR="$BUILD_DIR/repackage"
|
||||||
|
|
||||||
|
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||||
|
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||||
|
|
||||||
|
# Check if a .deb file was found
|
||||||
|
if [ -z "$DEB_FILE" ]; then
|
||||||
|
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the filename from the path for later use
|
||||||
|
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||||
|
|
||||||
|
echo "Found .deb file: $DEB_FILENAME"
|
||||||
|
|
||||||
|
# Create a temp directory inside build to avoid file conflicts
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
|
||||||
|
# Extract the .deb file contents
|
||||||
|
ar x "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Decompress zst files to tar
|
||||||
|
unzstd control.tar.zst
|
||||||
|
unzstd data.tar.zst
|
||||||
|
|
||||||
|
# Compress tar files to xz
|
||||||
|
xz -c control.tar > control.tar.xz
|
||||||
|
xz -c data.tar > data.tar.xz
|
||||||
|
|
||||||
|
# Remove the original .deb file
|
||||||
|
rm "../../$DEB_FILE"
|
||||||
|
|
||||||
|
# Create the new .deb file with xz compression in the original location
|
||||||
|
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||||
|
|
||||||
|
# Clean up temp files
|
||||||
|
cd ../..
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
echo "Repackaging complete: $DEB_FILENAME"
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
rootProject.name = 'sparrow'
|
rootProject.name = 'sparrow'
|
||||||
include 'drongo'
|
include 'drongo'
|
||||||
|
include 'lark'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
mime-type=x-scheme-handler/aopp
|
|
||||||
description=Verify Address Ownership URI
|
|
||||||
3
src/main/deploy/asc.properties
Normal file
3
src/main/deploy/asc.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mime-type=application/pgp-signature
|
||||||
|
extension=asc
|
||||||
|
description=ASCII Armored File
|
||||||
2
src/main/deploy/auth47.properties
Normal file
2
src/main/deploy/auth47.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
mime-type=x-scheme-handler/auth47
|
||||||
|
description=Auth47 Authentication URI
|
||||||
2
src/main/deploy/lightning.properties
Normal file
2
src/main/deploy/lightning.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
mime-type=x-scheme-handler/lightning
|
||||||
|
description=LNURL URI
|
||||||
12
src/main/deploy/package/linux-headless/control
Normal file
12
src/main/deploy/package/linux-headless/control
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Package: sparrowserver
|
||||||
|
Version: ${version}-1
|
||||||
|
Section: utils
|
||||||
|
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||||
|
Priority: optional
|
||||||
|
Architecture: ${arch}
|
||||||
|
Conflicts: sparrow (<= 2.1.4)
|
||||||
|
Replaces: sparrow (<= 2.1.4)
|
||||||
|
Provides: sparrowserver
|
||||||
|
Description: Sparrow Server
|
||||||
|
Depends: libc6, zlib1g
|
||||||
|
Installed-Size: ${size}
|
||||||
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
|
|
@ -0,0 +1,85 @@
|
||||||
|
Summary: Sparrow Server
|
||||||
|
Name: sparrowserver
|
||||||
|
Version: ${version}
|
||||||
|
Release: 1
|
||||||
|
License: ASL 2.0
|
||||||
|
Vendor: Unknown
|
||||||
|
|
||||||
|
%if "x" != "x"
|
||||||
|
URL: https://sparrowwallet.com
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%if "x/opt" != "x"
|
||||||
|
Prefix: /opt
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Provides: sparrowserver
|
||||||
|
Obsoletes: sparrow <= 2.1.4
|
||||||
|
|
||||||
|
%if "xutils" != "x"
|
||||||
|
Group: utils
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Autoprov: 0
|
||||||
|
Autoreq: 0
|
||||||
|
|
||||||
|
#comment line below to enable effective jar compression
|
||||||
|
#it could easily get your package size from 40 to 15Mb but
|
||||||
|
#build time will substantially increase and it may require unpack200/system java to install
|
||||||
|
%define __jar_repack %{nil}
|
||||||
|
|
||||||
|
# on RHEL we got unwanted improved debugging enhancements
|
||||||
|
%define _build_id_links none
|
||||||
|
|
||||||
|
%define package_filelist %{_builddir}/%{name}.files
|
||||||
|
%define app_filelist %{_builddir}/%{name}.app.files
|
||||||
|
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||||
|
|
||||||
|
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||||
|
|
||||||
|
%description
|
||||||
|
Sparrow Server
|
||||||
|
|
||||||
|
%global __os_install_post %{nil}
|
||||||
|
|
||||||
|
%prep
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
rm -rf %{buildroot}
|
||||||
|
install -d -m 755 %{buildroot}/opt/sparrowserver
|
||||||
|
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
||||||
|
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||||
|
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||||
|
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||||
|
fi
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||||
|
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||||
|
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||||
|
%endif
|
||||||
|
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||||
|
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||||
|
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||||
|
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||||
|
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%files -f %{package_filelist}
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
%license "%{license_install_file}"
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%post
|
||||||
|
package_type=rpm
|
||||||
|
|
||||||
|
%pre
|
||||||
|
package_type=rpm
|
||||||
|
|
||||||
|
%preun
|
||||||
|
package_type=rpm
|
||||||
|
|
||||||
|
%clean
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Sparrow
|
Name=Sparrow
|
||||||
Comment=Sparrow
|
Comment=Sparrow
|
||||||
Exec=/opt/sparrow/bin/Sparrow %U
|
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||||
Icon=/opt/sparrow/lib/Sparrow.png
|
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Unknown
|
Categories=Finance;Network;
|
||||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
|
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||||
|
StartupWMClass=Sparrow
|
||||||
|
SingleMainWindow=true
|
||||||
|
|
|
||||||
12
src/main/deploy/package/linux/control
Normal file
12
src/main/deploy/package/linux/control
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Package: sparrowwallet
|
||||||
|
Version: ${version}-1
|
||||||
|
Section: utils
|
||||||
|
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||||
|
Priority: optional
|
||||||
|
Architecture: ${arch}
|
||||||
|
Provides: sparrowwallet
|
||||||
|
Conflicts: sparrow (<= 2.1.4)
|
||||||
|
Replaces: sparrow (<= 2.1.4)
|
||||||
|
Description: Sparrow Wallet
|
||||||
|
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
||||||
|
Installed-Size: ${size}
|
||||||
49
src/main/deploy/package/linux/postinst
Executable file
49
src/main/deploy/package/linux/postinst
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# postinst script for sparrowwallet
|
||||||
|
#
|
||||||
|
# see: dh_installdeb(1)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# summary of how this script can be called:
|
||||||
|
# * <postinst> `configure' <most-recently-configured-version>
|
||||||
|
# * <old-postinst> `abort-upgrade' <new version>
|
||||||
|
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||||
|
# <new-version>
|
||||||
|
# * <postinst> `abort-remove'
|
||||||
|
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||||
|
# <failed-install-package> <version> `removing'
|
||||||
|
# <conflicting-package> <version>
|
||||||
|
# for details, see https://www.debian.org/doc/debian-policy/ or
|
||||||
|
# the debian-policy package
|
||||||
|
|
||||||
|
package_type=deb
|
||||||
|
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
|
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
|
if ! getent group plugdev > /dev/null; then
|
||||||
|
groupadd 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
|
||||||
260
src/main/deploy/package/linux/sparrowwallet.spec
Executable file
260
src/main/deploy/package/linux/sparrowwallet.spec
Executable file
|
|
@ -0,0 +1,260 @@
|
||||||
|
Summary: Sparrow
|
||||||
|
Name: sparrowwallet
|
||||||
|
Version: ${version}
|
||||||
|
Release: 1
|
||||||
|
License: ASL 2.0
|
||||||
|
Vendor: Unknown
|
||||||
|
|
||||||
|
%if "x" != "x"
|
||||||
|
URL: https://sparrowwallet.com
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%if "x/opt" != "x"
|
||||||
|
Prefix: /opt
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Provides: sparrowwallet
|
||||||
|
Obsoletes: sparrow <= 2.1.4
|
||||||
|
|
||||||
|
%if "xutils" != "x"
|
||||||
|
Group: utils
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Autoprov: 0
|
||||||
|
Autoreq: 0
|
||||||
|
%if "xxdg-utils" != "x" || "x" != "x"
|
||||||
|
Requires: xdg-utils
|
||||||
|
%endif
|
||||||
|
|
||||||
|
#comment line below to enable effective jar compression
|
||||||
|
#it could easily get your package size from 40 to 15Mb but
|
||||||
|
#build time will substantially increase and it may require unpack200/system java to install
|
||||||
|
%define __jar_repack %{nil}
|
||||||
|
|
||||||
|
# on RHEL we got unwanted improved debugging enhancements
|
||||||
|
%define _build_id_links none
|
||||||
|
|
||||||
|
%define package_filelist %{_builddir}/%{name}.files
|
||||||
|
%define app_filelist %{_builddir}/%{name}.app.files
|
||||||
|
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||||
|
|
||||||
|
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||||
|
|
||||||
|
%description
|
||||||
|
Sparrow Wallet
|
||||||
|
|
||||||
|
%global __os_install_post %{nil}
|
||||||
|
|
||||||
|
%prep
|
||||||
|
|
||||||
|
%build
|
||||||
|
|
||||||
|
%install
|
||||||
|
rm -rf %{buildroot}
|
||||||
|
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||||
|
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||||
|
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||||
|
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||||
|
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||||
|
fi
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||||
|
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||||
|
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||||
|
%endif
|
||||||
|
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||||
|
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||||
|
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||||
|
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||||
|
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%files -f %{package_filelist}
|
||||||
|
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||||
|
%license "%{license_install_file}"
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%post
|
||||||
|
package_type=rpm
|
||||||
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
|
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
|
if ! getent group plugdev > /dev/null; then
|
||||||
|
groupadd 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>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.5.2</string>
|
<string>2.3.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||||
|
|
@ -33,8 +33,12 @@
|
||||||
<string>Copyright (C) 2021</string>
|
<string>Copyright (C) 2021</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
|
<key>NSCameraUseContinuityCameraDeviceType</key>
|
||||||
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
<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>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
@ -47,10 +51,18 @@
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>com.sparrowwallet.sparrow.aopp</string>
|
<string>com.sparrowwallet.sparrow.auth47</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>aopp</string>
|
<string>auth47</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.sparrowwallet.sparrow.lightning</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>lightning</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
|
@ -86,6 +98,21 @@
|
||||||
<key>UTTypeIconFile</key>
|
<key>UTTypeIconFile</key>
|
||||||
<string>sparrow.icns</string>
|
<string>sparrow.icns</string>
|
||||||
</dict>
|
</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>
|
</array>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,9 @@
|
||||||
|
|
||||||
<DirectoryRef Id="TARGETDIR">
|
<DirectoryRef Id="TARGETDIR">
|
||||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||||
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
|
<RegistryKey Root="HKCR" Key="auth47" Action="createAndRemoveOnUninstall">
|
||||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||||
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
|
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||||
<RegistryKey Key="DefaultIcon">
|
<RegistryKey Key="DefaultIcon">
|
||||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
|
|
@ -97,6 +97,16 @@
|
||||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
|
<RegistryKey Root="HKCR" Key="lightning" Action="createAndRemoveOnUninstall">
|
||||||
|
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||||
|
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||||
|
<RegistryKey Key="DefaultIcon">
|
||||||
|
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||||
|
</RegistryKey>
|
||||||
|
<RegistryKey Key="shell\open\command">
|
||||||
|
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||||
|
</RegistryKey>
|
||||||
|
</RegistryKey>
|
||||||
</Component>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class AboutController {
|
||||||
private Label title;
|
private Label title;
|
||||||
|
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
|
title.setText(SparrowWallet.APP_NAME + " " + SparrowWallet.APP_VERSION + SparrowWallet.APP_VERSION_SUFFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStage(Stage stage) {
|
public void setStage(Stage stage) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,9 @@ import com.beust.jcommander.Parameter;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import org.slf4j.event.Level;
|
import org.slf4j.event.Level;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Args {
|
public class Args {
|
||||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||||
public String dir;
|
public String dir;
|
||||||
|
|
@ -14,6 +17,31 @@ public class Args {
|
||||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||||
public Level level;
|
public Level level;
|
||||||
|
|
||||||
|
@Parameter(names = { "--terminal", "-t" }, description = "Terminal mode", arity = 0)
|
||||||
|
public boolean terminal;
|
||||||
|
|
||||||
|
@Parameter(names = { "--version", "-v" }, description = "Show version", arity = 0)
|
||||||
|
public boolean version;
|
||||||
|
|
||||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||||
public boolean help;
|
public boolean help;
|
||||||
|
|
||||||
|
public List<String> toParams() {
|
||||||
|
List<String> params = new ArrayList<>();
|
||||||
|
|
||||||
|
if(dir != null) {
|
||||||
|
params.add("-d");
|
||||||
|
params.add(dir);
|
||||||
|
}
|
||||||
|
if(network != null) {
|
||||||
|
params.add("-n");
|
||||||
|
params.add(network.toString());
|
||||||
|
}
|
||||||
|
if(level != null) {
|
||||||
|
params.add("-l");
|
||||||
|
params.add(level.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@ import org.fxmisc.richtext.event.MouseOverTextEvent;
|
||||||
import org.fxmisc.richtext.model.TwoDimensional;
|
import org.fxmisc.richtext.model.TwoDimensional;
|
||||||
|
|
||||||
import java.time.Duration;
|
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;
|
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
|
||||||
|
|
||||||
public abstract class BaseController {
|
public abstract class BaseController {
|
||||||
|
|
@ -24,14 +29,11 @@ public abstract class BaseController {
|
||||||
|
|
||||||
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||||
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
ScriptChunk hoverChunk = getScriptChunk(scriptArea, e.getCharacterIndex());
|
||||||
if(position.getMajor() % 2 == 0) {
|
if(hoverChunk != null) {
|
||||||
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
|
Point2D pos = e.getScreenPosition();
|
||||||
if(!hoverChunk.isOpCode()) {
|
popupMsg.setText(describeScriptChunk(hoverChunk));
|
||||||
Point2D pos = e.getScreenPosition();
|
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
||||||
popupMsg.setText(describeScriptChunk(hoverChunk));
|
|
||||||
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
||||||
|
|
@ -80,4 +82,26 @@ public abstract class BaseController {
|
||||||
|
|
||||||
return "Invalid";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class BlockSummary implements Comparable<BlockSummary> {
|
||||||
|
private final Integer height;
|
||||||
|
private final Date timestamp;
|
||||||
|
private final Double medianFee;
|
||||||
|
private final Integer transactionCount;
|
||||||
|
private final Integer weight;
|
||||||
|
|
||||||
|
public BlockSummary(Integer height, Date timestamp) {
|
||||||
|
this(height, timestamp, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
||||||
|
this.height = height;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.medianFee = medianFee;
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
this.weight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Double> getMedianFee() {
|
||||||
|
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getTransactionCount() {
|
||||||
|
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getWeight() {
|
||||||
|
return weight == null ? Optional.empty() : Optional.of(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||||
|
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||||
|
Instant nowInstant = Instant.now();
|
||||||
|
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getElapsed() {
|
||||||
|
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
||||||
|
if(elapsed < 0) {
|
||||||
|
return "now";
|
||||||
|
} else if(elapsed < 60) {
|
||||||
|
return elapsed + "s";
|
||||||
|
} else if(elapsed < 3600) {
|
||||||
|
return elapsed / 60 + "m";
|
||||||
|
} else if(elapsed < 86400) {
|
||||||
|
return elapsed / 3600 + "h";
|
||||||
|
} else {
|
||||||
|
return elapsed / 86400 + "d";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return getElapsed() + ":" + getMedianFee();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(BlockSummary o) {
|
||||||
|
return o.height - height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
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;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import org.controlsfx.control.HyperlinkLabel;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
||||||
|
public class DefaultInteractionServices implements InteractionServices {
|
||||||
|
@Override
|
||||||
|
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||||
|
Alert alert = new Alert(alertType, content, buttons);
|
||||||
|
alert.initOwner(getActiveWindow());
|
||||||
|
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||||
|
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
alert.setTitle(title);
|
||||||
|
alert.setHeaderText(title);
|
||||||
|
if(graphic != null) {
|
||||||
|
alert.setGraphic(graphic);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||||
|
Matcher matcher = linkPattern.matcher(content);
|
||||||
|
if(matcher.find()) {
|
||||||
|
String link = matcher.group(1);
|
||||||
|
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||||
|
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
hyperlinkLabel.getStyleClass().add("content");
|
||||||
|
Label label = new Label();
|
||||||
|
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||||
|
hyperlinkLabel.setOnAction(event -> {
|
||||||
|
alert.close();
|
||||||
|
AppServices.get().getApplication().getHostServices().showDocument(link);
|
||||||
|
});
|
||||||
|
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] lines = content.split("\r\n|\r|\n");
|
||||||
|
if(lines.length > 3 || 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.setResizable(true);
|
||||||
|
|
||||||
|
moveToActiveWindowScreen(alert);
|
||||||
|
return alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||||
|
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(walletName, keystore);
|
||||||
|
passphraseDialog.initOwner(getActiveWindow());
|
||||||
|
return passphraseDialog.showAndWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface InteractionServices {
|
||||||
|
Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons);
|
||||||
|
Optional<String> requestPassphrase(String walletName, Keystore keystore);
|
||||||
|
}
|
||||||
34
src/main/java/com/sparrowwallet/sparrow/Interface.java
Normal file
34
src/main/java/com/sparrowwallet/sparrow/Interface.java
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
public enum Interface {
|
||||||
|
DESKTOP, TERMINAL, SERVER;
|
||||||
|
|
||||||
|
private static Interface currentInterface;
|
||||||
|
|
||||||
|
public static Interface get() {
|
||||||
|
if(currentInterface == null) {
|
||||||
|
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||||
|
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||||
|
|
||||||
|
if(headless || monocle) {
|
||||||
|
currentInterface = TERMINAL;
|
||||||
|
|
||||||
|
if(headless && !monocle) {
|
||||||
|
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentInterface = DESKTOP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set(Interface interf) {
|
||||||
|
if(currentInterface != null && interf != currentInterface) {
|
||||||
|
throw new IllegalStateException("Interface already set to " + currentInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInterface = interf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow;
|
|
||||||
|
|
||||||
import com.beust.jcommander.JCommander;
|
|
||||||
import com.sparrowwallet.drongo.Drongo;
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
|
||||||
import com.sparrowwallet.sparrow.net.Bwt;
|
|
||||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
|
||||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
|
||||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
|
||||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
|
||||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.scene.text.Font;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
|
||||||
import org.controlsfx.tools.Platform;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class MainApp extends Application {
|
|
||||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
|
||||||
public static final String APP_NAME = "Sparrow";
|
|
||||||
public static final String APP_VERSION = "1.5.2";
|
|
||||||
public static final String APP_VERSION_SUFFIX = "";
|
|
||||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
|
||||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
|
||||||
|
|
||||||
private Stage mainStage;
|
|
||||||
|
|
||||||
private static SparrowInstance sparrowInstance;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws Exception {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
|
||||||
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
|
|
||||||
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
|
||||||
} else {
|
|
||||||
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
super.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
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);
|
|
||||||
|
|
||||||
AppServices.initialize(this);
|
|
||||||
|
|
||||||
boolean createNewWallet = false;
|
|
||||||
Mode mode = Config.get().getMode();
|
|
||||||
if(mode == null) {
|
|
||||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
|
||||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
|
||||||
if(optionalMode.isPresent()) {
|
|
||||||
mode = optionalMode.get();
|
|
||||||
Config.get().setMode(mode);
|
|
||||||
|
|
||||||
if(mode.equals(Mode.ONLINE)) {
|
|
||||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
|
||||||
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
|
||||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
|
||||||
} else if(Network.get() == Network.MAINNET) {
|
|
||||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
|
||||||
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)].getUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
|
|
||||||
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()));
|
|
||||||
|
|
||||||
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
|
|
||||||
mainStage.setWidth(Config.get().getAppWidth());
|
|
||||||
mainStage.setHeight(Config.get().getAppHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
AppController appController = AppServices.newAppWindow(stage);
|
|
||||||
|
|
||||||
if(createNewWallet) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
for(File walletFile : sortedWalletFiles) {
|
|
||||||
if(walletFile.exists()) {
|
|
||||||
appController.openWalletFile(walletFile, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppServices.openFileUriArguments(stage);
|
|
||||||
|
|
||||||
AppServices.get().start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
AppServices.get().stop();
|
|
||||||
Config.get().setAppWidth(mainStage.getWidth());
|
|
||||||
Config.get().setAppHeight(mainStage.getHeight());
|
|
||||||
mainStage.close();
|
|
||||||
if(sparrowInstance != null) {
|
|
||||||
sparrowInstance.freeLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] argv) {
|
|
||||||
Args args = new Args();
|
|
||||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase()).acceptUnknownOptions(true).build();
|
|
||||||
jCommander.parse(argv);
|
|
||||||
if(args.help) {
|
|
||||||
jCommander.usage();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(args.level != null) {
|
|
||||||
Drongo.setRootLogLevel(args.level);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(args.dir != null) {
|
|
||||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
|
||||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(args.network != null) {
|
|
||||||
Network.set(args.network);
|
|
||||||
} else {
|
|
||||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
|
||||||
if(envNetwork != null) {
|
|
||||||
try {
|
|
||||||
Network.set(Network.valueOf(envNetwork.toUpperCase()));
|
|
||||||
} catch(Exception e) {
|
|
||||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
|
||||||
if(testnetFlag.exists()) {
|
|
||||||
Network.set(Network.TESTNET);
|
|
||||||
}
|
|
||||||
|
|
||||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
|
||||||
if(signetFlag.exists()) {
|
|
||||||
Network.set(Network.SIGNET);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Network.get() != Network.MAINNET) {
|
|
||||||
getLogger().info("Using " + Network.get() + " configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
|
||||||
|
|
||||||
try {
|
|
||||||
sparrowInstance = new SparrowInstance(fileUriArguments);
|
|
||||||
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
|
||||||
} catch(InstanceException e) {
|
|
||||||
getLogger().error("Could not access application lock", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!fileUriArguments.isEmpty()) {
|
|
||||||
AppServices.parseFileUriArguments(fileUriArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
|
||||||
SLF4JBridgeHandler.install();
|
|
||||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Logger getLogger() {
|
|
||||||
return LoggerFactory.getLogger(MainApp.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SparrowInstance extends InstanceList {
|
|
||||||
private final List<String> fileUriArguments;
|
|
||||||
|
|
||||||
public SparrowInstance(List<String> fileUriArguments) {
|
|
||||||
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
|
||||||
this.fileUriArguments = fileUriArguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void receiveMessageList(List<String> messageList) {
|
|
||||||
if(messageList != null && !messageList.isEmpty()) {
|
|
||||||
AppServices.parseFileUriArguments(messageList);
|
|
||||||
AppServices.openFileUriArguments(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> sendMessageList() {
|
|
||||||
return fileUriArguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void beforeExit() {
|
|
||||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
134
src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java
Normal file
134
src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
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;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
|
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.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.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class SparrowDesktop extends Application {
|
||||||
|
private Stage mainStage;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws Exception {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||||
|
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
|
||||||
|
LoggerFactory.getLogger(SparrowWallet.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||||
|
} else {
|
||||||
|
LoggerFactory.getLogger(SparrowWallet.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage stage) throws Exception {
|
||||||
|
this.mainStage = stage;
|
||||||
|
|
||||||
|
initializeFonts();
|
||||||
|
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||||
|
|
||||||
|
AppServices.initialize(this);
|
||||||
|
|
||||||
|
boolean createNewWallet = false;
|
||||||
|
Mode mode = Config.get().getMode();
|
||||||
|
if(mode == null) {
|
||||||
|
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||||
|
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||||
|
if(optionalMode.isPresent()) {
|
||||||
|
mode = optionalMode.get();
|
||||||
|
Config.get().setMode(mode);
|
||||||
|
|
||||||
|
if(mode.equals(Mode.ONLINE)) {
|
||||||
|
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);
|
||||||
|
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||||
|
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getServer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
|
||||||
|
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
mainStage.setHeight(Config.get().getAppHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
AppController appController = AppServices.newAppWindow(stage);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() throws Exception {
|
||||||
|
AppServices.get().stop();
|
||||||
|
Config.get().setAppWidth(mainStage.getWidth());
|
||||||
|
Config.get().setAppHeight(mainStage.getHeight());
|
||||||
|
mainStage.close();
|
||||||
|
SparrowWallet.Instance instance = SparrowWallet.getSparrowInstance();
|
||||||
|
if(instance != null) {
|
||||||
|
instance.freeLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
Normal file
160
src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.beust.jcommander.JCommander;
|
||||||
|
import com.sparrowwallet.drongo.Drongo;
|
||||||
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||||
|
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sun.javafx.application.PlatformImpl;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SparrowWallet {
|
||||||
|
public static final String APP_ID = "sparrow";
|
||||||
|
public static final String APP_NAME = "Sparrow";
|
||||||
|
public static final String APP_VERSION = "2.3.1";
|
||||||
|
public static final String APP_VERSION_SUFFIX = "";
|
||||||
|
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||||
|
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||||
|
|
||||||
|
private static Instance instance;
|
||||||
|
|
||||||
|
public static void main(String[] argv) {
|
||||||
|
Args args = new Args();
|
||||||
|
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||||
|
jCommander.parse(argv);
|
||||||
|
if(args.help) {
|
||||||
|
jCommander.usage();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.version) {
|
||||||
|
System.out.println("Sparrow Wallet " + APP_VERSION);
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.level != null) {
|
||||||
|
Drongo.setRootLogLevel(args.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.dir != null) {
|
||||||
|
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||||
|
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.network != null) {
|
||||||
|
Network.set(args.network);
|
||||||
|
} else {
|
||||||
|
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||||
|
if(envNetwork != null) {
|
||||||
|
try {
|
||||||
|
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||||
|
} catch(Exception e) {
|
||||||
|
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||||
|
if(testnetFlag.exists()) {
|
||||||
|
Network.set(Network.TESTNET);
|
||||||
|
}
|
||||||
|
|
||||||
|
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||||
|
if(testnet4Flag.exists()) {
|
||||||
|
Network.set(Network.TESTNET4);
|
||||||
|
}
|
||||||
|
|
||||||
|
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||||
|
if(signetFlag.exists()) {
|
||||||
|
Network.set(Network.SIGNET);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Network.get() != Network.MAINNET) {
|
||||||
|
getLogger().info("Using " + Network.get() + " configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||||
|
|
||||||
|
try {
|
||||||
|
instance = new Instance(fileUriArguments);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!fileUriArguments.isEmpty()) {
|
||||||
|
AppServices.parseFileUriArguments(fileUriArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||||
|
SLF4JBridgeHandler.install();
|
||||||
|
|
||||||
|
if(args.terminal) {
|
||||||
|
Interface.set(Interface.TERMINAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(Interface.get() == Interface.TERMINAL) {
|
||||||
|
PlatformImpl.setTaskbarApplication(false);
|
||||||
|
Drongo.removeRootLogAppender("STDOUT");
|
||||||
|
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowTerminal.class, SparrowWalletPreloader.class, argv);
|
||||||
|
} else {
|
||||||
|
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowDesktop.class, SparrowWalletPreloader.class, argv);
|
||||||
|
}
|
||||||
|
} catch(UnsupportedOperationException e) {
|
||||||
|
Drongo.removeRootLogAppender("STDOUT");
|
||||||
|
getLogger().error("Unable to launch application", e);
|
||||||
|
System.out.println("No display detected. Use Sparrow Server on a headless (no display) system.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(instance != null) {
|
||||||
|
instance.freeLock();
|
||||||
|
}
|
||||||
|
} catch(InstanceException instanceException) {
|
||||||
|
getLogger().error("Unable to free instance lock", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Instance getSparrowInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Logger getLogger() {
|
||||||
|
return LoggerFactory.getLogger(SparrowWallet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Instance extends InstanceList {
|
||||||
|
private final List<String> fileUriArguments;
|
||||||
|
|
||||||
|
public Instance(List<String> fileUriArguments) {
|
||||||
|
super(SparrowWallet.APP_ID, true);
|
||||||
|
this.fileUriArguments = fileUriArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void receiveMessageList(List<String> messageList) {
|
||||||
|
if(messageList != null) {
|
||||||
|
AppServices.parseFileUriArguments(messageList);
|
||||||
|
AppServices.openFileUriArguments(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> sendMessageList() {
|
||||||
|
return fileUriArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeExit() {
|
||||||
|
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||||
import javafx.application.Preloader;
|
import javafx.application.Preloader;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
public class MainAppPreloader extends Preloader {
|
public class SparrowWalletPreloader extends Preloader {
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) {
|
public void start(Stage stage) {
|
||||||
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
|
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow;
|
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.LogHandler;
|
|
||||||
import com.sparrowwallet.sparrow.event.TorStatusEvent;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.slf4j.event.Level;
|
|
||||||
|
|
||||||
public class TorLogHandler implements LogHandler {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
|
|
||||||
log.debug(message);
|
|
||||||
EventManager.get().post(new TorStatusEvent(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
120
src/main/java/com/sparrowwallet/sparrow/UnitFormat.java
Normal file
120
src/main/java/com/sparrowwallet/sparrow/UnitFormat.java
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum UnitFormat {
|
||||||
|
DOT {
|
||||||
|
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||||
|
|
||||||
|
public DecimalFormat getBtcFormat() {
|
||||||
|
btcFormat.setMaximumFractionDigits(8);
|
||||||
|
return btcFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getSatsFormat() {
|
||||||
|
return satsFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getTableBtcFormat() {
|
||||||
|
return tableBtcFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getCurrencyFormat() {
|
||||||
|
return currencyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getTableCurrencyFormat() {
|
||||||
|
return tableCurrencyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||||
|
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||||
|
symbols.setDecimalSeparator('.');
|
||||||
|
symbols.setGroupingSeparator(',');
|
||||||
|
return symbols;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
COMMA {
|
||||||
|
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||||
|
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||||
|
|
||||||
|
public DecimalFormat getBtcFormat() {
|
||||||
|
btcFormat.setMaximumFractionDigits(8);
|
||||||
|
return btcFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getSatsFormat() {
|
||||||
|
return satsFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getTableBtcFormat() {
|
||||||
|
return tableBtcFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getCurrencyFormat() {
|
||||||
|
return currencyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getTableCurrencyFormat() {
|
||||||
|
return tableCurrencyFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||||
|
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||||
|
symbols.setDecimalSeparator(',');
|
||||||
|
symbols.setGroupingSeparator('.');
|
||||||
|
return symbols;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract DecimalFormatSymbols getDecimalFormatSymbols();
|
||||||
|
|
||||||
|
public abstract DecimalFormat getBtcFormat();
|
||||||
|
|
||||||
|
public abstract DecimalFormat getSatsFormat();
|
||||||
|
|
||||||
|
public abstract DecimalFormat getTableBtcFormat();
|
||||||
|
|
||||||
|
public abstract DecimalFormat getCurrencyFormat();
|
||||||
|
|
||||||
|
public abstract DecimalFormat getTableCurrencyFormat();
|
||||||
|
|
||||||
|
public String formatBtcValue(Long amount) {
|
||||||
|
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String tableFormatBtcValue(Long amount) {
|
||||||
|
return getTableBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatSatsValue(Long amount) {
|
||||||
|
return getSatsFormat().format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String formatCurrencyValue(double amount) {
|
||||||
|
return getCurrencyFormat().format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String tableFormatCurrencyValue(double amount) {
|
||||||
|
return getTableCurrencyFormat().format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroupingSeparator() {
|
||||||
|
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDecimalSeparator() {
|
||||||
|
return Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,8 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||||
welcomeController.initializeView();
|
welcomeController.initializeView();
|
||||||
|
|
||||||
dialogPane.setPrefWidth(600);
|
dialogPane.setPrefWidth(600);
|
||||||
dialogPane.setPrefHeight(520);
|
dialogPane.setPrefHeight(540);
|
||||||
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,19 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
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.collections.FXCollections;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
|
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||||
|
|
||||||
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
|
private static final int MAX_SHOWN_ACCOUNTS = 8;
|
||||||
|
|
||||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||||
private boolean discoverAccounts = false;
|
private boolean discoverAccounts = false;
|
||||||
|
|
||||||
|
|
@ -42,27 +42,32 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
standardAccountCombo = new ComboBox<>();
|
standardAccountCombo = new ComboBox<>();
|
||||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
List<Integer> existingIndexes = new ArrayList<>();
|
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
||||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
existingIndexes.add(masterWallet.getAccountIndex());
|
existingIndexes.add(masterWallet.getAccountIndex());
|
||||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||||
existingIndexes.add(childWallet.getAccountIndex());
|
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<>();
|
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount) && availableAccounts.size() <= MAX_SHOWN_ACCOUNTS) {
|
||||||
availableAccounts.add(standardAccount);
|
availableAccounts.add(standardAccount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||||
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
|
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||||
|
} 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);
|
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
|
||||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||||
|
|
@ -80,10 +85,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
return "None Available";
|
return "None Available";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
|
if(account == WHIRLPOOL_PREMIX) {
|
||||||
return "Whirlpool Accounts";
|
return "Whirlpool Accounts";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(account == WHIRLPOOL_POSTMIX) {
|
||||||
|
return "Whirlpool Postmix (No mixing)";
|
||||||
|
}
|
||||||
|
|
||||||
return account.getName();
|
return account.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Status;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.Hyperlink;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.TreeTableCell;
|
import javafx.scene.control.TreeTableCell;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
public AddressCell() {
|
public AddressCell() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -33,12 +40,16 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
if(utxoEntry != null) {
|
if(utxoEntry != null) {
|
||||||
Address address = addressStatus.getAddress();
|
Address address = addressStatus.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()), false, getTreeTableView()));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
|
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
getStyleClass().add("address-cell");
|
||||||
|
|
||||||
if(addressStatus.isDuplicate()) {
|
if(addressStatus.isDustAttack()) {
|
||||||
|
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||||
|
} else if(addressStatus.isDuplicate()) {
|
||||||
setGraphic(getDuplicateGlyph());
|
setGraphic(getDuplicateGlyph());
|
||||||
} else {
|
} else {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
|
@ -47,8 +58,9 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate, boolean dustAttack) {
|
||||||
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
|
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
|
||||||
|
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : (dustAttack ? " (Possible dust attack)" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Glyph getDuplicateGlyph() {
|
public static Glyph getDuplicateGlyph() {
|
||||||
|
|
@ -57,4 +69,22 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||||
duplicateGlyph.setFontSize(12);
|
duplicateGlyph.setFontSize(12);
|
||||||
return duplicateGlyph;
|
return duplicateGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Hyperlink getDustAttackHyperlink(UtxoEntry utxoEntry) {
|
||||||
|
Glyph dustAttackGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||||
|
dustAttackGlyph.getStyleClass().add("dust-attack-warning");
|
||||||
|
dustAttackGlyph.setFontSize(12);
|
||||||
|
|
||||||
|
Hyperlink hyperlink = new Hyperlink(utxoEntry.getHashIndex().getStatus() == Status.FROZEN ? "" : "Freeze?", dustAttackGlyph);
|
||||||
|
hyperlink.getStyleClass().add("freeze-dust-utxo");
|
||||||
|
hyperlink.setOnAction(event -> {
|
||||||
|
if(utxoEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||||
|
hyperlink.setText("");
|
||||||
|
utxoEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||||
|
EventManager.get().post(new WalletUtxoStatusChangedEvent(utxoEntry.getWallet(), Collections.singletonList(utxoEntry.getHashIndex())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return hyperlink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
||||||
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
|
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.ShowTransactionsCountEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
|
@ -13,14 +15,12 @@ import javafx.collections.ListChangeListener;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.OptionalInt;
|
|
||||||
|
|
||||||
public class AddressTreeTable extends CoinTreeTable {
|
public class AddressTreeTable extends CoinTreeTable {
|
||||||
public void initialize(NodeEntry rootEntry) {
|
public void initialize(NodeEntry rootEntry) {
|
||||||
getStyleClass().add("address-treetable");
|
getStyleClass().add("address-treetable");
|
||||||
setBitcoinUnit(rootEntry.getWallet());
|
setUnitFormat(rootEntry.getWallet());
|
||||||
|
|
||||||
String address = rootEntry.getAddress().toString();
|
String address = rootEntry.getAddress().toString();
|
||||||
updateAll(rootEntry);
|
updateAll(rootEntry);
|
||||||
|
|
@ -46,6 +46,15 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
labelCol.setSortable(false);
|
labelCol.setSortable(false);
|
||||||
getColumns().add(labelCol);
|
getColumns().add(labelCol);
|
||||||
|
|
||||||
|
TreeTableColumn<Entry, Number> countCol = new TreeTableColumn<>("Transactions");
|
||||||
|
countCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||||
|
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getChildren().size());
|
||||||
|
});
|
||||||
|
countCol.setCellFactory(p -> new NumberCell());
|
||||||
|
countCol.setSortable(false);
|
||||||
|
countCol.setVisible(Config.get().isShowAddressTransactionCount());
|
||||||
|
getColumns().add(countCol);
|
||||||
|
|
||||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||||
|
|
@ -54,8 +63,21 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
amountCol.setSortable(false);
|
amountCol.setSortable(false);
|
||||||
getColumns().add(amountCol);
|
getColumns().add(amountCol);
|
||||||
|
|
||||||
|
ContextMenu contextMenu = new ContextMenu();
|
||||||
|
CheckMenuItem showCountItem = new CheckMenuItem("Show Transaction Count");
|
||||||
|
contextMenu.setOnShowing(event -> {
|
||||||
|
showCountItem.setSelected(Config.get().isShowAddressTransactionCount());
|
||||||
|
});
|
||||||
|
showCountItem.setOnAction(event -> {
|
||||||
|
boolean show = !Config.get().isShowAddressTransactionCount();
|
||||||
|
Config.get().setShowAddressTransactionCount(show);
|
||||||
|
EventManager.get().post(new ShowTransactionsCountEvent(show));
|
||||||
|
});
|
||||||
|
contextMenu.getItems().add(showCountItem);
|
||||||
|
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||||
|
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
setupColumnWidths();
|
||||||
|
|
||||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||||
getSortOrder().add(addressCol);
|
getSortOrder().add(addressCol);
|
||||||
|
|
@ -92,7 +114,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAll(NodeEntry rootEntry) {
|
public void updateAll(NodeEntry rootEntry) {
|
||||||
setBitcoinUnit(rootEntry.getWallet());
|
setUnitFormat(rootEntry.getWallet());
|
||||||
|
|
||||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||||
setRoot(rootItem);
|
setRoot(rootItem);
|
||||||
|
|
@ -106,17 +128,38 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||||
//We only ever add child nodes - never remove in order to keep a full sequence
|
//We only ever add child nodes - never remove in order to keep a full sequence (unless hide empty used addresses is set)
|
||||||
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
||||||
|
|
||||||
|
Map<WalletNode, NodeEntry> childNodes = new HashMap<>();
|
||||||
|
for(Entry childEntry : rootEntry.getChildren()) {
|
||||||
|
NodeEntry nodeEntry = (NodeEntry)childEntry;
|
||||||
|
childNodes.put(nodeEntry.getNode(), nodeEntry);
|
||||||
|
}
|
||||||
|
|
||||||
for(WalletNode updatedNode : updatedNodes) {
|
for(WalletNode updatedNode : updatedNodes) {
|
||||||
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
|
NodeEntry existingEntry = childNodes.get(updatedNode);
|
||||||
if(optEntry.isPresent()) {
|
if(existingEntry != null) {
|
||||||
NodeEntry existingEntry = (NodeEntry)optEntry.get();
|
|
||||||
existingEntry.refreshChildren();
|
existingEntry.refreshChildren();
|
||||||
|
|
||||||
|
if(Config.get().isHideEmptyUsedAddresses() && existingEntry.getValue() == 0L) {
|
||||||
|
rootEntry.getChildren().remove(existingEntry);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||||
rootEntry.getChildren().add(nodeEntry);
|
|
||||||
|
if(Config.get().isHideEmptyUsedAddresses()) {
|
||||||
|
int index = 0;
|
||||||
|
for( ; index < rootEntry.getChildren().size(); index++) {
|
||||||
|
existingEntry = (NodeEntry)rootEntry.getChildren().get(index);
|
||||||
|
if(nodeEntry.compareTo(existingEntry) < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootEntry.getChildren().add(index, nodeEntry);
|
||||||
|
} else {
|
||||||
|
rootEntry.getChildren().add(nodeEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,4 +170,8 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||||
Entry rootEntry = getRoot().getValue();
|
Entry rootEntry = getRoot().getValue();
|
||||||
rootEntry.updateLabel(entry);
|
rootEntry.updateLabel(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showTransactionsCount(boolean show) {
|
||||||
|
getColumns().stream().filter(col -> col.getText().equals("Transactions")).forEach(col -> col.setVisible(show));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import javafx.animation.Animation;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
public class AnimationUtil {
|
||||||
|
public static Timeline getSlowFadeOut(Node node, Duration duration, double fromValue, int numIncrements) {
|
||||||
|
Timeline fadeTimeline = new Timeline();
|
||||||
|
Duration incrementDuration = duration.divide(numIncrements);
|
||||||
|
for(int i = 0; i < numIncrements; i++) {
|
||||||
|
double percent = ((double)numIncrements - i - 1) / numIncrements;
|
||||||
|
double opacity = percent * fromValue;
|
||||||
|
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fadeTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Timeline getPulse(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
|
||||||
|
Timeline pulseTimeline = getFade(node, duration, fromValue, toValue, numIncrements);
|
||||||
|
|
||||||
|
pulseTimeline.setCycleCount(Animation.INDEFINITE);
|
||||||
|
pulseTimeline.setAutoReverse(true);
|
||||||
|
|
||||||
|
return pulseTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Timeline getFade(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
|
||||||
|
Timeline fadeTimeline = new Timeline();
|
||||||
|
Duration incrementDuration = duration.divide(numIncrements);
|
||||||
|
for(int i = 0; i < numIncrements; i++) {
|
||||||
|
double percent = ((double) numIncrements - i - 1) / numIncrements; //From 99% to 0%
|
||||||
|
double opacity = (percent * (fromValue - toValue)) + toValue;
|
||||||
|
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fadeTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AnimatedNode (Node node, Timeline timeline) {}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,27 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.chart.*;
|
import javafx.scene.chart.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class BalanceChart extends LineChart<Number, Number> {
|
public class BalanceChart extends LineChart<Number, Number> {
|
||||||
|
private static final int MAX_VALUES = 500;
|
||||||
|
|
||||||
private XYChart.Series<Number, Number> balanceSeries;
|
private XYChart.Series<Number, Number> balanceSeries;
|
||||||
|
|
||||||
private TransactionEntry selectedEntry;
|
private TransactionEntry selectedEntry;
|
||||||
|
|
@ -29,15 +36,14 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||||
getData().add(balanceSeries);
|
getData().add(balanceSeries);
|
||||||
update(walletTransactionsEntry);
|
update(walletTransactionsEntry);
|
||||||
|
|
||||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
setUnitFormat(walletTransactionsEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||||
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(WalletTransactionsEntry walletTransactionsEntry) {
|
public void update(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
|
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
|
||||||
balanceSeries.getData().clear();
|
balanceSeries.getData().clear();
|
||||||
|
|
||||||
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
|
List<Data<Number, Number>> balanceDataList = getTransactionEntries(walletTransactionsEntry)
|
||||||
.map(entry -> (TransactionEntry)entry)
|
.map(entry -> (TransactionEntry)entry)
|
||||||
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
|
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
|
||||||
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
|
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
|
||||||
|
|
@ -74,6 +80,24 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Stream<Entry> getTransactionEntries(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
|
int total = walletTransactionsEntry.getChildren().size();
|
||||||
|
if(walletTransactionsEntry.getChildren().size() <= MAX_VALUES) {
|
||||||
|
return walletTransactionsEntry.getChildren().stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
int bucketSize = total / MAX_VALUES;
|
||||||
|
List<List<Entry>> buckets = Lists.partition(walletTransactionsEntry.getChildren(), bucketSize);
|
||||||
|
List<Entry> reducedEntries = new ArrayList<>(MAX_VALUES);
|
||||||
|
for(List<Entry> bucket : buckets) {
|
||||||
|
long max = bucket.stream().mapToLong(entry -> Math.abs(entry.getValue())).max().orElse(0);
|
||||||
|
Entry bucketEntry = bucket.stream().filter(entry -> entry.getValue() == max || entry.getValue() == -max).findFirst().orElseThrow();
|
||||||
|
reducedEntries.add(bucketEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reducedEntries.stream();
|
||||||
|
}
|
||||||
|
|
||||||
public void select(TransactionEntry transactionEntry) {
|
public void select(TransactionEntry transactionEntry) {
|
||||||
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
|
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
|
||||||
for(Node selectedSymbol : selectedSymbols) {
|
for(Node selectedSymbol : selectedSymbols) {
|
||||||
|
|
@ -92,12 +116,16 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||||
|
if(format == null) {
|
||||||
|
format = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
unit = wallet.getAutoUnit();
|
unit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
|
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
||||||
|
public class BitBoxPairingDialog extends Alert {
|
||||||
|
public BitBoxPairingDialog(String code) {
|
||||||
|
super(AlertType.INFORMATION);
|
||||||
|
initOwner(getActiveWindow());
|
||||||
|
setStageIcon(getDialogPane().getScene().getWindow());
|
||||||
|
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
setTitle("Confirm BitBox02 Pairing");
|
||||||
|
setHeaderText(getTitle());
|
||||||
|
VBox vBox = new VBox(20);
|
||||||
|
vBox.setAlignment(Pos.CENTER);
|
||||||
|
vBox.setPadding(new Insets(10, 20, 10, 20));
|
||||||
|
Label instructions = new Label("Confirm the following code is shown on BitBox02");
|
||||||
|
Label codeLabel = new Label(code);
|
||||||
|
codeLabel.getStyleClass().add("fixed-width");
|
||||||
|
vBox.getChildren().addAll(instructions, codeLabel);
|
||||||
|
getDialogPane().setContent(vBox);
|
||||||
|
moveToActiveWindowScreen(this);
|
||||||
|
getDialogPane().getButtonTypes().clear();
|
||||||
|
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.sparrow.BlockSummary;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.KeyValue;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.beans.property.*;
|
||||||
|
import javafx.scene.Group;
|
||||||
|
import javafx.scene.shape.Polygon;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.FontWeight;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.girod.javafx.svgimage.SVGImage;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BlockCube extends Group {
|
||||||
|
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
|
||||||
|
|
||||||
|
public static final double CUBE_SIZE = 60;
|
||||||
|
|
||||||
|
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
||||||
|
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
||||||
|
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
||||||
|
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
||||||
|
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
||||||
|
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
||||||
|
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
||||||
|
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
private Polygon front;
|
||||||
|
private Rectangle unusedArea;
|
||||||
|
private Rectangle usedArea;
|
||||||
|
|
||||||
|
private final Text heightText = new Text();
|
||||||
|
private final Text medianFeeText = new Text();
|
||||||
|
private final Text unitsText = new Text();
|
||||||
|
private final TextFlow medianFeeTextFlow = new TextFlow();
|
||||||
|
private final Text txCountText = new Text();
|
||||||
|
private final Text elapsedText = new Text();
|
||||||
|
private final Group feeRateIcon = new Group();
|
||||||
|
|
||||||
|
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
||||||
|
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
||||||
|
this.confirmedProperty.set(confirmed);
|
||||||
|
|
||||||
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
this.feeRatesSource.set(feeRatesSource);
|
||||||
|
|
||||||
|
this.weightProperty.addListener((_, _, _) -> {
|
||||||
|
if(front != null) {
|
||||||
|
updateFill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
||||||
|
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||||
|
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
||||||
|
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
||||||
|
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
||||||
|
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
||||||
|
});
|
||||||
|
this.txCountProperty.addListener((_, _, newValue) -> {
|
||||||
|
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
||||||
|
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
});
|
||||||
|
this.timestampProperty.addListener((_, _, newValue) -> {
|
||||||
|
elapsedProperty.set(getElapsed(newValue.longValue()));
|
||||||
|
});
|
||||||
|
this.elapsedProperty.addListener((_, _, newValue) -> {
|
||||||
|
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
|
||||||
|
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
});
|
||||||
|
this.heightProperty.addListener((_, _, newValue) -> {
|
||||||
|
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
|
||||||
|
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
});
|
||||||
|
this.confirmedProperty.addListener((_, _, _) -> {
|
||||||
|
if(front != null) {
|
||||||
|
updateFill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.feeRatesSource.addListener((_, _, _) -> {
|
||||||
|
if(front != null) {
|
||||||
|
updateFill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
||||||
|
pulse();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(weight != null) {
|
||||||
|
this.weightProperty.set(weight);
|
||||||
|
}
|
||||||
|
if(medianFee != null) {
|
||||||
|
this.medianFeeProperty.set(medianFee);
|
||||||
|
}
|
||||||
|
if(height != null) {
|
||||||
|
this.heightProperty.set(height);
|
||||||
|
}
|
||||||
|
if(txCount != null) {
|
||||||
|
this.txCountProperty.set(txCount);
|
||||||
|
}
|
||||||
|
if(timestamp != null) {
|
||||||
|
this.timestampProperty.set(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCube();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCube() {
|
||||||
|
double depth = CUBE_SIZE * 0.2;
|
||||||
|
double perspective = CUBE_SIZE * 0.04;
|
||||||
|
|
||||||
|
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
|
||||||
|
front.getStyleClass().add("block-front");
|
||||||
|
front.setFill(null);
|
||||||
|
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||||
|
unusedArea.getStyleClass().add("block-unused");
|
||||||
|
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||||
|
usedArea.getStyleClass().add("block-used");
|
||||||
|
|
||||||
|
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
|
||||||
|
|
||||||
|
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
|
||||||
|
top.getStyleClass().add("block-top");
|
||||||
|
top.setStroke(null);
|
||||||
|
|
||||||
|
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
|
||||||
|
left.getStyleClass().add("block-left");
|
||||||
|
left.setStroke(null);
|
||||||
|
|
||||||
|
updateFill();
|
||||||
|
|
||||||
|
heightText.getStyleClass().add("block-height");
|
||||||
|
heightText.setFont(new Font(11));
|
||||||
|
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
heightText.setY(-24);
|
||||||
|
|
||||||
|
medianFeeText.getStyleClass().add("block-text");
|
||||||
|
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
|
||||||
|
unitsText.getStyleClass().add("block-text");
|
||||||
|
unitsText.setFont(new Font(10));
|
||||||
|
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
|
||||||
|
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
|
||||||
|
medianFeeTextFlow.setTranslateY(7);
|
||||||
|
|
||||||
|
txCountText.getStyleClass().add("block-text");
|
||||||
|
txCountText.setFont(new Font(10));
|
||||||
|
txCountText.setOpacity(0.7);
|
||||||
|
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
txCountText.setY(34);
|
||||||
|
|
||||||
|
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
||||||
|
feeRateIcon.setTranslateY(-36);
|
||||||
|
|
||||||
|
elapsedText.getStyleClass().add("block-text");
|
||||||
|
elapsedText.setFont(new Font(10));
|
||||||
|
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||||
|
elapsedText.setY(50);
|
||||||
|
|
||||||
|
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFill() {
|
||||||
|
if(isConfirmed()) {
|
||||||
|
getStyleClass().removeAll("block-unconfirmed");
|
||||||
|
if(!getStyleClass().contains("block-confirmed")) {
|
||||||
|
getStyleClass().add("block-confirmed");
|
||||||
|
}
|
||||||
|
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
|
||||||
|
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
|
||||||
|
unusedArea.setHeight(startYAbsolute);
|
||||||
|
unusedArea.setStyle(null);
|
||||||
|
usedArea.setY(startYAbsolute);
|
||||||
|
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
||||||
|
usedArea.setVisible(true);
|
||||||
|
heightText.setVisible(true);
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
|
} else {
|
||||||
|
getStyleClass().removeAll("block-confirmed");
|
||||||
|
if(!getStyleClass().contains("block-unconfirmed")) {
|
||||||
|
getStyleClass().add("block-unconfirmed");
|
||||||
|
}
|
||||||
|
usedArea.setVisible(false);
|
||||||
|
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||||
|
heightText.setVisible(false);
|
||||||
|
if(feeRatesSource.get() != null) {
|
||||||
|
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
||||||
|
if(svgImage != null) {
|
||||||
|
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
||||||
|
} else {
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
feeRateIcon.getChildren().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pulse() {
|
||||||
|
if(isConfirmed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(unusedArea != null) {
|
||||||
|
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||||
|
}
|
||||||
|
|
||||||
|
Timeline timeline = new Timeline(
|
||||||
|
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
|
||||||
|
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
|
||||||
|
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
|
||||||
|
);
|
||||||
|
|
||||||
|
timeline.setCycleCount(1);
|
||||||
|
timeline.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||||
|
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||||
|
Instant nowInstant = Instant.now();
|
||||||
|
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getElapsed(long timestampUtc) {
|
||||||
|
long elapsed = calculateElapsedSeconds(timestampUtc);
|
||||||
|
if(elapsed < 60) {
|
||||||
|
return "Just now";
|
||||||
|
} else if(elapsed < 3600) {
|
||||||
|
return Math.round(elapsed / 60f) + "m ago";
|
||||||
|
} else if(elapsed < 86400) {
|
||||||
|
return Math.round(elapsed / 3600f) + "h ago";
|
||||||
|
} else {
|
||||||
|
return Math.round(elapsed / 86400d) + "d ago";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFeeRateStyleName() {
|
||||||
|
double rate = getMedianFee();
|
||||||
|
int[] feeRateInterval = getFeeRateInterval(rate);
|
||||||
|
if(feeRateInterval[1] == Integer.MAX_VALUE) {
|
||||||
|
return "VSIZE2000-2200_COLOR";
|
||||||
|
}
|
||||||
|
int[] nextRateInterval = getFeeRateInterval(rate * 2);
|
||||||
|
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
|
||||||
|
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
|
||||||
|
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] getFeeRateInterval(double medianFee) {
|
||||||
|
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
|
||||||
|
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
|
||||||
|
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
|
||||||
|
if(feeRate <= medianFee && nextFeeRate > medianFee) {
|
||||||
|
return new int[] { feeRate, nextFeeRate };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new int[] { 1, 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWeight() {
|
||||||
|
return weightProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty weightProperty() {
|
||||||
|
return weightProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWeight(int weight) {
|
||||||
|
weightProperty.set(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMedianFee() {
|
||||||
|
return medianFeeProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DoubleProperty medianFee() {
|
||||||
|
return medianFeeProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedianFee(double medianFee) {
|
||||||
|
medianFeeProperty.set(medianFee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return heightProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty heightProperty() {
|
||||||
|
return heightProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeight(int height) {
|
||||||
|
heightProperty.set(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTxCount() {
|
||||||
|
return txCountProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty txCountProperty() {
|
||||||
|
return txCountProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTxCount(int txCount) {
|
||||||
|
txCountProperty.set(txCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestampProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongProperty timestampProperty() {
|
||||||
|
return timestampProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(long timestamp) {
|
||||||
|
timestampProperty.set(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getElapsed() {
|
||||||
|
return elapsedProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty elapsedProperty() {
|
||||||
|
return elapsedProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setElapsed(String elapsed) {
|
||||||
|
elapsedProperty.set(elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfirmed() {
|
||||||
|
return confirmedProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty confirmedProperty() {
|
||||||
|
return confirmedProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmed(boolean confirmed) {
|
||||||
|
confirmedProperty.set(confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeeRatesSource getFeeRatesSource() {
|
||||||
|
return feeRatesSource.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
||||||
|
return feeRatesSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||||
|
this.feeRatesSource.set(feeRatesSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
||||||
|
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
||||||
|
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.chart.Axis;
|
import javafx.scene.chart.Axis;
|
||||||
|
|
@ -28,7 +28,7 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||||
|
|
||||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||||
Integer targetBlocks = targetBlocksIter.next();
|
Integer targetBlocks = targetBlocksIter.next();
|
||||||
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||||
feeRateSeries.getData().add(data);
|
feeRateSeries.getData().add(data);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,367 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||||
|
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
import org.controlsfx.validation.Validator;
|
||||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.smartcardio.CardException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
||||||
|
|
||||||
|
public class CardImportPane extends TitledDescriptionPane {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||||
|
|
||||||
|
private final KeystoreCardImport importer;
|
||||||
|
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)), importer.getWalletModel());
|
||||||
|
this.importer = importer;
|
||||||
|
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Control createButton() {
|
||||||
|
importButton = new Button("Import");
|
||||||
|
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||||
|
tapGlyph.setFontSize(12);
|
||||||
|
importButton.setGraphic(tapGlyph);
|
||||||
|
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
importButton.setOnAction(event -> {
|
||||||
|
importButton.setDisable(true);
|
||||||
|
importCard();
|
||||||
|
});
|
||||||
|
return importButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importCard() {
|
||||||
|
if(!isReaderAvailable()) {
|
||||||
|
setError("No reader", "No card reader was detected.");
|
||||||
|
importButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringProperty messageProperty = new SimpleStringProperty();
|
||||||
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(pin.get().length() < importer.getWalletModel().getMinPinLength()) {
|
||||||
|
setDescription(pin.get().isEmpty() ? (!importer.getWalletModel().hasDefaultPin() && !importer.isInitialized() ? "Choose a PIN code" : "Enter PIN code") : "PIN code too short");
|
||||||
|
setContent(getPinAndDerivationEntry());
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
importButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!importer.isInitialized()) {
|
||||||
|
setDescription("Card not initialized");
|
||||||
|
setContent(getInitializationPanel(messageProperty));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(CardException e) {
|
||||||
|
setError("Card Error", e.getMessage());
|
||||||
|
importButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
|
||||||
|
cardImportService.setOnSucceeded(event -> {
|
||||||
|
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||||
|
});
|
||||||
|
cardImportService.setOnFailed(event -> {
|
||||||
|
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||||
|
if(rootCause instanceof CardAuthorizationException) {
|
||||||
|
setError(rootCause.getMessage(), null);
|
||||||
|
setContent(getPinAndDerivationEntry());
|
||||||
|
} else {
|
||||||
|
log.error("Error importing keystore from card", event.getSource().getException());
|
||||||
|
setError("Import Error", rootCause.getMessage());
|
||||||
|
}
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardImportService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getInitializationPanel(StringProperty messageProperty) {
|
||||||
|
if(importer.getWalletModel().requiresSeedInitialization()) {
|
||||||
|
return getSeedInitializationPanel(messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEntropyInitializationPanel(messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getSeedInitializationPanel(StringProperty messageProperty) {
|
||||||
|
VBox confirmationBox = new VBox(5);
|
||||||
|
CustomPasswordField confirmationPin = new ViewPasswordField();
|
||||||
|
confirmationPin.setPromptText("Re-enter chosen PIN");
|
||||||
|
confirmationBox.getChildren().add(confirmationPin);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
if(!pin.get().equals(confirmationPin.getText())) {
|
||||||
|
setError("PIN Error", "The confirmation PIN did not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pinSize = pin.get().length();
|
||||||
|
if(pinSize < importer.getWalletModel().getMinPinLength() || pinSize > importer.getWalletModel().getMaxPinLength()) {
|
||||||
|
setError("PIN Error", "PIN length must be between " + importer.getWalletModel().getMinPinLength() + " and " + importer.getWalletModel().getMaxPinLength() + " characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(importer.getWalletModel().toDisplayString() + " Seed Words", 12);
|
||||||
|
seedEntryDialog.initOwner(this.getScene().getWindow());
|
||||||
|
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||||
|
if(optWords.isPresent()) {
|
||||||
|
try {
|
||||||
|
List<String> mnemonicWords = optWords.get();
|
||||||
|
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
|
||||||
|
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, "", System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||||
|
byte[] seedBytes = seed.getSeedBytes();
|
||||||
|
|
||||||
|
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), seedBytes, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
setDescription("Leave card on reader");
|
||||||
|
setExpanded(false);
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
log.error("Invalid seed entered", e);
|
||||||
|
AppServices.showErrorDialog("Invalid seed entered", "The seed was invalid.\n\n" + e.getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(confirmationBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(confirmationBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getEntropyInitializationPanel(StringProperty messageProperty) {
|
||||||
|
VBox initTypeBox = new VBox(5);
|
||||||
|
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||||
|
RadioButton advanced = new RadioButton("Advanced");
|
||||||
|
TextField entropy = new TextField();
|
||||||
|
entropy.setPromptText("Enter input for user entropy");
|
||||||
|
entropy.setDisable(true);
|
||||||
|
|
||||||
|
ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
automatic.setToggleGroup(toggleGroup);
|
||||||
|
advanced.setToggleGroup(toggleGroup);
|
||||||
|
automatic.setSelected(true);
|
||||||
|
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
entropy.setDisable(newValue == automatic);
|
||||||
|
});
|
||||||
|
|
||||||
|
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||||
|
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
setDescription("Leave card on reader");
|
||||||
|
setExpanded(false);
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||||
|
if(rootCause instanceof CardAuthorizationException) {
|
||||||
|
setError(rootCause.getMessage(), null);
|
||||||
|
setContent(getPinEntry());
|
||||||
|
importButton.setDisable(false);
|
||||||
|
} else {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getPinAndDerivationEntry() {
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
vBox.getChildren().add(getPinEntry());
|
||||||
|
vBox.getChildren().add(getDerivationEntry());
|
||||||
|
return vBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getPinEntry() {
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
|
||||||
|
CustomPasswordField pinField = new ViewPasswordField();
|
||||||
|
pinField.setPromptText("PIN Code");
|
||||||
|
importButton.setDefaultButton(true);
|
||||||
|
pin.bind(pinField.textProperty());
|
||||||
|
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||||
|
Platform.runLater(pinField::requestFocus);
|
||||||
|
|
||||||
|
HBox contentBox = new HBox();
|
||||||
|
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||||
|
contentBox.setSpacing(20);
|
||||||
|
contentBox.getChildren().add(pinField);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 0, 30));
|
||||||
|
contentBox.setPrefHeight(50);
|
||||||
|
|
||||||
|
vBox.getChildren().add(contentBox);
|
||||||
|
|
||||||
|
return vBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getDerivationEntry() {
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
|
||||||
|
CheckBox checkBox = new CheckBox("Use Custom Derivation");
|
||||||
|
Label customLabel = new Label("Derivation:");
|
||||||
|
TextField customDerivation = new TextField(KeyDerivation.writePath(derivation));
|
||||||
|
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
validationSupport.registerValidator(customDerivation, Validator.combine(
|
||||||
|
Validator.createEmptyValidator("Derivation is required"),
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||||
|
));
|
||||||
|
|
||||||
|
customDerivation.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue.isEmpty() || !KeyDerivation.isValid(newValue)) {
|
||||||
|
importButton.setDisable(true);
|
||||||
|
} else {
|
||||||
|
importButton.setDisable(false);
|
||||||
|
derivation = KeyDerivation.parsePath(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkBox.managedProperty().bind(checkBox.visibleProperty());
|
||||||
|
customLabel.managedProperty().bind(customLabel.visibleProperty());
|
||||||
|
customDerivation.managedProperty().bind(customDerivation.visibleProperty());
|
||||||
|
customLabel.visibleProperty().bind(checkBox.visibleProperty().not());
|
||||||
|
customDerivation.visibleProperty().bind(checkBox.visibleProperty().not());
|
||||||
|
|
||||||
|
checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
checkBox.setVisible(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox derivationBox = new HBox();
|
||||||
|
derivationBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
derivationBox.setSpacing(20);
|
||||||
|
derivationBox.getChildren().addAll(checkBox, customLabel, customDerivation);
|
||||||
|
derivationBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
derivationBox.setPrefHeight(50);
|
||||||
|
|
||||||
|
vBox.getChildren().addAll(derivationBox);
|
||||||
|
|
||||||
|
return vBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CardInitializationService extends Service<Void> {
|
||||||
|
private final KeystoreCardImport cardImport;
|
||||||
|
private final String pin;
|
||||||
|
private final byte[] chainCode;
|
||||||
|
private final StringProperty messageProperty;
|
||||||
|
|
||||||
|
public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) {
|
||||||
|
this.cardImport = cardImport;
|
||||||
|
this.pin = pin;
|
||||||
|
this.chainCode = chainCode;
|
||||||
|
this.messageProperty = messageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() throws Exception {
|
||||||
|
cardImport.initialize(pin, chainCode, messageProperty);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CardImportService extends Service<Keystore> {
|
||||||
|
private final KeystoreCardImport cardImport;
|
||||||
|
private final String pin;
|
||||||
|
private final List<ChildNumber> derivation;
|
||||||
|
private final StringProperty messageProperty;
|
||||||
|
|
||||||
|
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||||
|
this.cardImport = cardImport;
|
||||||
|
this.pin = pin;
|
||||||
|
this.derivation = derivation;
|
||||||
|
this.messageProperty = messageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Keystore> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Keystore call() throws Exception {
|
||||||
|
return cardImport.getKeystore(pin, derivation, messageProperty);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
|
import org.controlsfx.glyphfont.FontAwesome;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
import tornadofx.control.Field;
|
||||||
|
import tornadofx.control.Fieldset;
|
||||||
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
|
public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||||
|
private final CustomPasswordField existingPin;
|
||||||
|
private final CustomPasswordField newPin;
|
||||||
|
private final CustomPasswordField newPinConfirm;
|
||||||
|
private final CheckBox backupFirst;
|
||||||
|
private final ButtonType okButtonType;
|
||||||
|
|
||||||
|
public CardPinDialog(WalletModel walletModel, boolean backupOnly) {
|
||||||
|
this.existingPin = new ViewPasswordField();
|
||||||
|
this.newPin = new ViewPasswordField();
|
||||||
|
this.newPinConfirm = new ViewPasswordField();
|
||||||
|
this.backupFirst = new CheckBox();
|
||||||
|
|
||||||
|
if(backupOnly) {
|
||||||
|
newPin.textProperty().bind(existingPin.textProperty());
|
||||||
|
newPinConfirm.textProperty().bind(existingPin.textProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
setTitle(backupOnly ? "Backup Card" : "Change Card PIN");
|
||||||
|
dialogPane.setHeaderText(backupOnly ? "Enter the current card PIN." : "Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||||
|
dialogPane.setPrefWidth(380);
|
||||||
|
dialogPane.setPrefHeight(backupOnly ? 135 : 260);
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||||
|
lock.setFontSize(50);
|
||||||
|
dialogPane.setGraphic(lock);
|
||||||
|
|
||||||
|
Form form = new Form();
|
||||||
|
Fieldset fieldset = new Fieldset();
|
||||||
|
fieldset.setText("");
|
||||||
|
fieldset.setSpacing(10);
|
||||||
|
|
||||||
|
Field currentField = new Field();
|
||||||
|
currentField.setText("Current PIN:");
|
||||||
|
currentField.getInputs().add(existingPin);
|
||||||
|
|
||||||
|
Field newField = new Field();
|
||||||
|
newField.setText("New PIN:");
|
||||||
|
newField.getInputs().add(newPin);
|
||||||
|
|
||||||
|
Field confirmField = new Field();
|
||||||
|
confirmField.setText("Confirm new PIN:");
|
||||||
|
confirmField.getInputs().add(newPinConfirm);
|
||||||
|
|
||||||
|
Field backupField = new Field();
|
||||||
|
backupField.setText("Backup First:");
|
||||||
|
backupField.getInputs().add(backupFirst);
|
||||||
|
|
||||||
|
if(backupOnly) {
|
||||||
|
fieldset.getChildren().addAll(currentField);
|
||||||
|
} else {
|
||||||
|
fieldset.getChildren().addAll(currentField, newField, confirmField);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(walletModel.supportsBackup()) {
|
||||||
|
fieldset.getChildren().add(backupField);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.getChildren().add(fieldset);
|
||||||
|
dialogPane.setContent(form);
|
||||||
|
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
Platform.runLater( () -> {
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < walletModel.getMinPinLength() || existingPin.getText().length() > walletModel.getMaxPinLength()));
|
||||||
|
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < walletModel.getMinPinLength() || newPin.getText().length() > walletModel.getMaxPinLength()));
|
||||||
|
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||||
|
});
|
||||||
|
|
||||||
|
okButtonType = new javafx.scene.control.ButtonType(backupOnly ? "Backup" : "Change", ButtonBar.ButtonData.OK_DONE);
|
||||||
|
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||||
|
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||||
|
okButton.setPrefWidth(130);
|
||||||
|
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < walletModel.getMinPinLength() || existingPin.getText().length() > walletModel.getMaxPinLength()
|
||||||
|
|| newPin.getText().length() < walletModel.getMinPinLength() || newPin.getText().length() > walletModel.getMaxPinLength()
|
||||||
|
|| !newPin.getText().equals(newPinConfirm.getText()),
|
||||||
|
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
||||||
|
okButton.disableProperty().bind(isInvalid);
|
||||||
|
|
||||||
|
Platform.runLater(existingPin::requestFocus);
|
||||||
|
setResultConverter(dialogButton -> dialogButton == okButtonType ? new CardPinChange(existingPin.getText(), newPin.getText(), backupFirst.isSelected()) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CardPinChange(String currentPin, String newPin, boolean backupFirst) { }
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
import javafx.scene.chart.NumberAxis;
|
import javafx.scene.chart.NumberAxis;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
|
||||||
final class CoinAxisFormatter extends StringConverter<Number> {
|
final class CoinAxisFormatter extends StringConverter<Number> {
|
||||||
|
private final UnitFormat unitFormat;
|
||||||
private final BitcoinUnit bitcoinUnit;
|
private final BitcoinUnit bitcoinUnit;
|
||||||
|
|
||||||
public CoinAxisFormatter(NumberAxis axis, BitcoinUnit unit) {
|
public CoinAxisFormatter(NumberAxis axis, UnitFormat format, BitcoinUnit unit) {
|
||||||
|
this.unitFormat = format;
|
||||||
this.bitcoinUnit = unit;
|
this.bitcoinUnit = unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Number object) {
|
public String toString(Number object) {
|
||||||
Double value = bitcoinUnit.getValue(object.longValue());
|
Double value = bitcoinUnit.getValue(object.longValue());
|
||||||
return CoinTextFormatter.COIN_FORMAT.format(value);
|
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Number fromString(String string) {
|
public Number fromString(String string) {
|
||||||
try {
|
try {
|
||||||
Number number = CoinTextFormatter.COIN_FORMAT.parse(string);
|
Number number = new CoinTextFormatter(unitFormat).getCoinFormat().parse(string);
|
||||||
return bitcoinUnit.getSatsValue(number.doubleValue());
|
return bitcoinUnit.getSatsValue(number.doubleValue());
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,38 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.scene.control.TreeTableCell;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import org.controlsfx.tools.Platform;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.DecimalFormatSymbols;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsListener {
|
||||||
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
private final CoinTooltip tooltip;
|
||||||
|
private final CoinContextMenu contextMenu;
|
||||||
|
|
||||||
private final Tooltip tooltip;
|
private IntegerProperty confirmationsProperty;
|
||||||
|
|
||||||
public CoinCell() {
|
public CoinCell() {
|
||||||
super();
|
super();
|
||||||
tooltip = new Tooltip();
|
tooltip = new CoinTooltip();
|
||||||
|
tooltip.setShowDelay(Duration.millis(500));
|
||||||
|
contextMenu = new CoinContextMenu();
|
||||||
getStyleClass().add("coin-cell");
|
getStyleClass().add("coin-cell");
|
||||||
if(Platform.getCurrent() == Platform.OSX) {
|
if(OsType.getCurrent() == OsType.MACOS) {
|
||||||
getStyleClass().add("number-field");
|
getStyleClass().add("number-field");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,34 +45,32 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
setTooltip(null);
|
setTooltip(null);
|
||||||
|
setContextMenu(null);
|
||||||
} else {
|
} else {
|
||||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
EntryCell.applyRowStyles(this, entry);
|
EntryCell.applyRowStyles(this, entry);
|
||||||
|
|
||||||
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
|
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
|
||||||
|
UnitFormat format = coinTreeTable.getUnitFormat();
|
||||||
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
||||||
|
|
||||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
|
String satsValue = format.formatSatsValue(amount.longValue());
|
||||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
|
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
|
||||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||||
|
|
||||||
if(unit.equals(BitcoinUnit.BTC)) {
|
if(unit.equals(BitcoinUnit.BTC)) {
|
||||||
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||||
setText(btcValue);
|
setText(btcValue);
|
||||||
} else {
|
} else {
|
||||||
tooltip.setText(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||||
setText(satsValue);
|
setText(satsValue);
|
||||||
}
|
}
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
String tooltipValue = tooltip.getText();
|
contextMenu.updateAmount(amount);
|
||||||
|
setContextMenu(contextMenu);
|
||||||
|
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry transactionEntry) {
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
|
||||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
|
||||||
|
|
||||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
|
||||||
});
|
|
||||||
|
|
||||||
if(transactionEntry.isConfirming()) {
|
if(transactionEntry.isConfirming()) {
|
||||||
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
||||||
|
|
@ -82,6 +87,8 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||||
} else if(entry instanceof UtxoEntry) {
|
} else if(entry instanceof UtxoEntry) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry) {
|
||||||
|
tooltip.hideConfirmations();
|
||||||
|
|
||||||
Region node = new Region();
|
Region node = new Region();
|
||||||
node.setPrefWidth(10);
|
node.setPrefWidth(10);
|
||||||
setGraphic(node);
|
setGraphic(node);
|
||||||
|
|
@ -95,4 +102,107 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IntegerProperty getConfirmationsProperty() {
|
||||||
|
if(confirmationsProperty == null) {
|
||||||
|
confirmationsProperty = new SimpleIntegerProperty();
|
||||||
|
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||||
|
getStyleClass().remove("confirming");
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmationsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CoinTooltip extends Tooltip {
|
||||||
|
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
|
||||||
|
private boolean showConfirmations;
|
||||||
|
private boolean isCoinbase;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
setTooltipText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty, boolean coinbase) {
|
||||||
|
showConfirmations = true;
|
||||||
|
isCoinbase = coinbase;
|
||||||
|
|
||||||
|
int confirmations = txEntryConfirmationsProperty.get();
|
||||||
|
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||||
|
confirmationsProperty.bind(txEntryConfirmationsProperty);
|
||||||
|
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
setTooltipText();
|
||||||
|
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
confirmationsProperty.set(confirmations);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTooltipText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hideConfirmations() {
|
||||||
|
showConfirmations = false;
|
||||||
|
isCoinbase = false;
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
|
||||||
|
setTooltipText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTooltipText() {
|
||||||
|
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfirmationsDescription() {
|
||||||
|
int confirmations = confirmationsProperty.get();
|
||||||
|
if(confirmations == 0) {
|
||||||
|
return "Unconfirmed in mempool";
|
||||||
|
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||||
|
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s") + (isCoinbase ? ", immature coinbase" : "");
|
||||||
|
} else {
|
||||||
|
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CoinContextMenu extends ContextMenu {
|
||||||
|
private Number amount;
|
||||||
|
|
||||||
|
public void updateAmount(Number amount) {
|
||||||
|
if(amount.equals(this.amount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.amount = amount;
|
||||||
|
getItems().clear();
|
||||||
|
|
||||||
|
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||||
|
copySatsValue.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(amount.toString());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||||
|
copyBtcValue.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||||
|
content.putString(format.formatBtcValue(amount.longValue()));
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import javafx.beans.property.LongProperty;
|
import javafx.beans.property.LongProperty;
|
||||||
import javafx.beans.property.SimpleLongProperty;
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
public class CoinLabel extends Label {
|
||||||
import java.text.DecimalFormatSymbols;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class CoinLabel extends CopyableLabel {
|
|
||||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
|
||||||
|
|
||||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||||
private final Tooltip tooltip;
|
private final Tooltip tooltip;
|
||||||
private final CoinContextMenu contextMenu;
|
private final CoinContextMenu contextMenu;
|
||||||
|
|
@ -28,7 +23,6 @@ public class CoinLabel extends CopyableLabel {
|
||||||
|
|
||||||
public CoinLabel(String text) {
|
public CoinLabel(String text) {
|
||||||
super(text);
|
super(text);
|
||||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
|
||||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||||
tooltip = new Tooltip();
|
tooltip = new Tooltip();
|
||||||
contextMenu = new CoinContextMenu();
|
contextMenu = new CoinContextMenu();
|
||||||
|
|
@ -58,8 +52,9 @@ public class CoinLabel extends CopyableLabel {
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
setContextMenu(contextMenu);
|
setContextMenu(contextMenu);
|
||||||
|
|
||||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||||
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
String satsValue = format.formatSatsValue(value) + " sats";
|
||||||
|
String btcValue = format.formatBtcValue(value) + " BTC";
|
||||||
|
|
||||||
BitcoinUnit unit = bitcoinUnit;
|
BitcoinUnit unit = bitcoinUnit;
|
||||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
|
|
@ -77,7 +72,7 @@ public class CoinLabel extends CopyableLabel {
|
||||||
|
|
||||||
private class CoinContextMenu extends ContextMenu {
|
private class CoinContextMenu extends ContextMenu {
|
||||||
public CoinContextMenu() {
|
public CoinContextMenu() {
|
||||||
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||||
copySatsValue.setOnAction(AE -> {
|
copySatsValue.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
|
@ -89,16 +84,12 @@ public class CoinLabel extends CopyableLabel {
|
||||||
copyBtcValue.setOnAction(AE -> {
|
copyBtcValue.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||||
|
content.putString(format.formatBtcValue(getValue()));
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
getItems().addAll(copySatsValue, copyBtcValue);
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DecimalFormat getBTCFormat() {
|
|
||||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
|
||||||
return BTC_FORMAT;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
import javafx.scene.control.TextFormatter;
|
import javafx.scene.control.TextFormatter;
|
||||||
import javafx.scene.control.TextInputControl;
|
import javafx.scene.control.TextInputControl;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.DecimalFormatSymbols;
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class CoinTextFormatter extends TextFormatter<String> {
|
public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
|
public CoinTextFormatter(UnitFormat unitFormat) {
|
||||||
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
super(new CoinFilter(unitFormat == null ? UnitFormat.DOT : unitFormat));
|
||||||
|
}
|
||||||
|
|
||||||
public CoinTextFormatter() {
|
public UnitFormat getUnitFormat() {
|
||||||
super(new CoinFilter());
|
return ((CoinFilter)getFilter()).unitFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecimalFormat getCoinFormat() {
|
||||||
|
return ((CoinFilter)getFilter()).coinFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class CoinFilter implements UnaryOperator<Change> {
|
private static class CoinFilter implements UnaryOperator<Change> {
|
||||||
|
private final UnitFormat unitFormat;
|
||||||
|
private final DecimalFormat coinFormat;
|
||||||
|
private final Pattern coinValidation;
|
||||||
|
|
||||||
|
public CoinFilter(UnitFormat unitFormat) {
|
||||||
|
this.unitFormat = unitFormat;
|
||||||
|
this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols());
|
||||||
|
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Change apply(Change change) {
|
public Change apply(Change change) {
|
||||||
String oldText = change.getControlText();
|
String oldText = change.getControlText();
|
||||||
|
|
@ -30,17 +45,23 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
|
|
||||||
String noFractionCommaText = newText;
|
String noFractionCommaText = newText;
|
||||||
int commasRemoved = 0;
|
int commasRemoved = 0;
|
||||||
int dotIndex = newText.indexOf(".");
|
int dotIndex = newText.indexOf(unitFormat.getDecimalSeparator());
|
||||||
if(dotIndex > -1) {
|
if(dotIndex > -1) {
|
||||||
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(",", "");
|
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
|
||||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!COIN_VALIDATION.matcher(noFractionCommaText).matches()) {
|
Matcher matcher = coinValidation.matcher(noFractionCommaText);
|
||||||
return null;
|
if(!matcher.matches()) {
|
||||||
|
matcher.reset();
|
||||||
|
if(matcher.find()) {
|
||||||
|
noFractionCommaText = matcher.group();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(",".equals(change.getText())) {
|
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,20 +69,20 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(change.isDeleted() && ",".equals(deleted) && change.getRangeStart() > 0) {
|
if(change.isDeleted() && unitFormat.getGroupingSeparator().equals(deleted) && change.getRangeStart() > 0) {
|
||||||
noFractionCommaText = noFractionCommaText.substring(0, change.getRangeStart() - 1) + noFractionCommaText.substring(change.getRangeEnd() - 1);
|
noFractionCommaText = noFractionCommaText.substring(0, change.getRangeStart() - 1) + noFractionCommaText.substring(change.getRangeEnd() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Number value = COIN_FORMAT.parse(noFractionCommaText);
|
Number value = coinFormat.parse(noFractionCommaText);
|
||||||
String correct = COIN_FORMAT.format(value.doubleValue());
|
String correct = coinFormat.format(value.doubleValue());
|
||||||
|
|
||||||
String compare = newText;
|
String compare = newText;
|
||||||
if(compare.contains(".") && compare.endsWith("0")) {
|
if(compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) {
|
||||||
compare = compare.replaceAll("0*$", "");
|
compare = compare.replaceAll("0*$", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(compare.endsWith(".")) {
|
if(compare.endsWith(unitFormat.getDecimalSeparator())) {
|
||||||
compare = compare.substring(0, compare.length() - 1);
|
compare = compare.substring(0, compare.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,11 +100,11 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||||
|
|
||||||
if(correct.length() != newText.length()) {
|
if(correct.length() != newText.length()) {
|
||||||
String postCorrect = correct.substring(Math.min(change.getCaretPosition(), correct.length()));
|
String postCorrect = correct.substring(Math.min(change.getCaretPosition(), correct.length()));
|
||||||
int commasAfter = postCorrect.length() - postCorrect.replace(",", "").length();
|
int commasAfter = postCorrect.length() - postCorrect.replace(unitFormat.getGroupingSeparator(), "").length();
|
||||||
int caretShift = change.isDeleted() && ".".equals(deleted) ? commasAfter : 0;
|
int caretShift = change.isDeleted() && unitFormat.getDecimalSeparator().equals(deleted) ? commasAfter : 0;
|
||||||
|
|
||||||
int caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
|
int caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
|
||||||
if(caret >= 0) {
|
if(caret >= 0 && caret <= change.getControlNewText().length()) {
|
||||||
change.setCaretPosition(caret);
|
change.setCaretPosition(caret);
|
||||||
change.setAnchor(caret);
|
change.setAnchor(caret);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,83 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.drongo.wallet.SortDirection;
|
||||||
|
import com.sparrowwallet.drongo.wallet.TableType;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
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.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
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.WalletDataChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
|
||||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.subjects.PublishSubject;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TreeTableView;
|
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
|
private TableType tableType;
|
||||||
private BitcoinUnit bitcoinUnit;
|
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() {
|
public BitcoinUnit getBitcoinUnit() {
|
||||||
return bitcoinUnit;
|
return bitcoinUnit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBitcoinUnit(BitcoinUnit bitcoinUnit) {
|
public UnitFormat getUnitFormat() {
|
||||||
this.bitcoinUnit = bitcoinUnit;
|
return unitFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBitcoinUnit(Wallet wallet) {
|
public void setUnitFormat(Wallet wallet) {
|
||||||
setBitcoinUnit(wallet, Config.get().getBitcoinUnit());
|
setUnitFormat(wallet, Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
public void setUnitFormat(Wallet wallet, UnitFormat format) {
|
||||||
|
setUnitFormat(wallet, format, Config.get().getBitcoinUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||||
|
if(format == null) {
|
||||||
|
format = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
unit = wallet.getAutoUnit();
|
unit = wallet.getAutoUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean changed = (bitcoinUnit != unit);
|
boolean changed = (unitFormat != format);
|
||||||
|
changed |= (bitcoinUnit != unit);
|
||||||
|
this.unitFormat = format;
|
||||||
this.bitcoinUnit = unit;
|
this.bitcoinUnit = unit;
|
||||||
|
|
||||||
if(changed && !getChildren().isEmpty()) {
|
if(changed && !getChildren().isEmpty()) {
|
||||||
|
|
@ -52,6 +85,18 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CurrencyRate getCurrencyRate() {
|
||||||
|
return currencyRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrencyRate(CurrencyRate currencyRate) {
|
||||||
|
this.currencyRate = currencyRate;
|
||||||
|
|
||||||
|
if(!getChildren().isEmpty()) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
|
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
|
||||||
if(getRoot() != null) {
|
if(getRoot() != null) {
|
||||||
Entry entry = getRoot().getValue();
|
Entry entry = getRoot().getValue();
|
||||||
|
|
@ -81,18 +126,18 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
Hyperlink hyperlink = new Hyperlink();
|
Hyperlink hyperlink = new Hyperlink();
|
||||||
hyperlink.setTranslateY(30);
|
hyperlink.setTranslateY(30);
|
||||||
hyperlink.setOnAction(event -> {
|
hyperlink.setOnAction(event -> {
|
||||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
|
||||||
|
dlg.initOwner(this.getScene().getWindow());
|
||||||
Optional<Date> optDate = dlg.showAndWait();
|
Optional<Date> optDate = dlg.showAndWait();
|
||||||
if(optDate.isPresent()) {
|
if(optDate.isPresent()) {
|
||||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
Wallet pastWallet = wallet.copy();
|
Wallet pastWallet = wallet.copy();
|
||||||
storage.backupTempWallet();
|
|
||||||
wallet.setBirthDate(optDate.get());
|
wallet.setBirthDate(optDate.get());
|
||||||
//Trigger background save of birthdate
|
//Trigger background save of birthdate
|
||||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||||
//Trigger full wallet rescan
|
//Trigger full wallet rescan
|
||||||
wallet.clearHistory();
|
wallet.clearHistory();
|
||||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(wallet.getBirthDate() == null) {
|
if(wallet.getBirthDate() == null) {
|
||||||
|
|
@ -108,4 +153,108 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
stackPane.setAlignment(Pos.CENTER);
|
stackPane.setAlignment(Pos.CENTER);
|
||||||
return stackPane;
|
return stackPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
|
||||||
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,40 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ComboBox;
|
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.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.controlsfx.control.textfield.CustomTextField;
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ComboBoxTextField extends CustomTextField {
|
public class ComboBoxTextField extends CustomTextField {
|
||||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
private boolean initialized;
|
||||||
|
private boolean comboShowing;
|
||||||
|
|
||||||
public ComboBoxTextField() {
|
public ComboBoxTextField() {
|
||||||
super();
|
super();
|
||||||
getStyleClass().add("combo-text-field");
|
getStyleClass().add("combo-text-field");
|
||||||
setupCopyButtonField(super.rightProperty());
|
setupComboButtonField(super.rightProperty());
|
||||||
|
|
||||||
|
disabledProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(comboProperty.isNotNull().get()) {
|
||||||
|
comboProperty.get().setVisible(!newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
private void setupComboButtonField(ObjectProperty<Node> rightProperty) {
|
||||||
Region showComboButton = new Region();
|
Region showComboButton = new Region();
|
||||||
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
|
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
|
||||||
StackPane showComboButtonPane = new StackPane(showComboButton);
|
StackPane showComboButtonPane = new StackPane(showComboButton);
|
||||||
|
|
@ -26,7 +42,21 @@ public class ComboBoxTextField extends CustomTextField {
|
||||||
showComboButtonPane.setCursor(Cursor.DEFAULT);
|
showComboButtonPane.setCursor(Cursor.DEFAULT);
|
||||||
showComboButtonPane.setOnMouseReleased(e -> {
|
showComboButtonPane.setOnMouseReleased(e -> {
|
||||||
if(comboProperty.isNotNull().get()) {
|
if(comboProperty.isNotNull().get()) {
|
||||||
comboProperty.get().show();
|
if(comboShowing) {
|
||||||
|
comboProperty.get().hide();
|
||||||
|
} else {
|
||||||
|
comboProperty.get().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
comboShowing = !comboShowing;
|
||||||
|
|
||||||
|
if(!initialized) {
|
||||||
|
comboProperty.get().valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
comboShowing = false;
|
||||||
|
Platform.runLater(() -> comboProperty.get().getSelectionModel().clearSelection());
|
||||||
|
});
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
|
||||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||||
this.comboProperty.set(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,10 +86,10 @@ public class ConfirmationProgressIndicator extends StackPane {
|
||||||
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
|
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
|
||||||
sequence.getChildren().add(upTickLineTimeline);
|
sequence.getChildren().add(upTickLineTimeline);
|
||||||
|
|
||||||
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(10), confirmationGroup);
|
Timeline groupFadeOut = AnimationUtil.getSlowFadeOut(confirmationGroup, Duration.minutes(10), 1.0, 10);
|
||||||
groupFadeOut.setFromValue(1);
|
|
||||||
groupFadeOut.setToValue(0);
|
|
||||||
sequence.getChildren().add(groupFadeOut);
|
sequence.getChildren().add(groupFadeOut);
|
||||||
|
|
||||||
|
confirmationsProperty().unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence.play();
|
sequence.play();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
|
||||||
|
public interface ConfirmationsListener {
|
||||||
|
IntegerProperty getConfirmationsProperty();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final LongProperty valueProperty() {
|
||||||
|
return valueProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final long getValue() {
|
||||||
|
return valueProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setValue(long value) {
|
||||||
|
this.valueProperty.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
refresh(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||||
|
setValueAsText(getValue(), unitFormat, bitcoinUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||||
|
setTooltip(tooltip);
|
||||||
|
setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
if(unitFormat == null) {
|
||||||
|
unitFormat = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
String satsValue = unitFormat.formatSatsValue(value) + " sats";
|
||||||
|
String btcValue = unitFormat.formatBtcValue(value) + " BTC";
|
||||||
|
|
||||||
|
BitcoinUnit unit = bitcoinUnit;
|
||||||
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
|
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bitcoinUnit = unit;
|
||||||
|
|
||||||
|
if(unit.equals(BitcoinUnit.BTC)) {
|
||||||
|
tooltip.setText(satsValue);
|
||||||
|
setText(btcValue);
|
||||||
|
} else {
|
||||||
|
tooltip.setText(btcValue);
|
||||||
|
setText(satsValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CoinContextMenu extends ContextMenu {
|
||||||
|
public CoinContextMenu() {
|
||||||
|
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||||
|
copySatsValue.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(Long.toString(getValue()));
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||||
|
copyBtcValue.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||||
|
content.putString(format.formatBtcValue(getValue()));
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().addAll(copySatsValue, copyBtcValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import javafx.animation.FadeTransition;
|
import javafx.animation.FadeTransition;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
@ -16,10 +23,37 @@ import org.controlsfx.control.textfield.CustomTextField;
|
||||||
public class CopyableTextField extends CustomTextField {
|
public class CopyableTextField extends CustomTextField {
|
||||||
private static final Duration FADE_DURATION = Duration.millis(350);
|
private static final Duration FADE_DURATION = Duration.millis(350);
|
||||||
|
|
||||||
|
private final ChangeListener<String> selectionListener = (textObservable, textOldValue, textNewValue) -> {
|
||||||
|
if(!textNewValue.isEmpty()) {
|
||||||
|
deselect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final EventHandler<MouseEvent> copyHandler = event -> {
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(getCopyText());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
|
||||||
|
Tooltip tooltip = new Tooltip("Copied!");
|
||||||
|
tooltip.show(this, event.getScreenX(), event.getScreenY());
|
||||||
|
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide()));
|
||||||
|
timeline.play();
|
||||||
|
};
|
||||||
|
|
||||||
public CopyableTextField() {
|
public CopyableTextField() {
|
||||||
super();
|
super();
|
||||||
getStyleClass().add("copyable-text-field");
|
getStyleClass().add("copyable-text-field");
|
||||||
setupCopyButtonField(super.rightProperty());
|
setupCopyButtonField(super.rightProperty());
|
||||||
|
editableProperty().addListener((observable, oldValue, editable) -> {
|
||||||
|
if(!editable) {
|
||||||
|
setOnMouseClicked(copyHandler);
|
||||||
|
selectedTextProperty().addListener(selectionListener);
|
||||||
|
} else {
|
||||||
|
setOnMouseClicked(null);
|
||||||
|
selectedTextProperty().removeListener(selectionListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setContextMenu(new ContextMenu());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||||
|
|
@ -31,7 +65,7 @@ public class CopyableTextField extends CustomTextField {
|
||||||
copyButtonPane.setCursor(Cursor.DEFAULT);
|
copyButtonPane.setCursor(Cursor.DEFAULT);
|
||||||
copyButtonPane.setOnMouseReleased(e -> {
|
copyButtonPane.setOnMouseReleased(e -> {
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(getText());
|
content.putString(getCopyText());
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -61,4 +95,8 @@ public class CopyableTextField extends CustomTextField {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getCopyText() {
|
||||||
|
return getText();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.control.EntryCell.HashIndexEntryContextMenu;
|
||||||
|
|
||||||
public class DateCell extends TreeTableCell<Entry, Entry> {
|
public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
|
|
@ -35,17 +37,18 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
||||||
setContextMenu(null);
|
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), utxoEntry));
|
||||||
} else if(utxoEntry.getHashIndex().getDate() != null) {
|
} else if(utxoEntry.getHashIndex().getDate() != null) {
|
||||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||||
setText(date);
|
setText(date);
|
||||||
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
|
setContextMenu(new DateContextMenu(getTreeTableView(), utxoEntry, date));
|
||||||
} else {
|
} else {
|
||||||
setText("Unknown");
|
setText("Unknown");
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
int height = utxoEntry.getHashIndex().getHeight();
|
int height = utxoEntry.getHashIndex().getHeight();
|
||||||
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
|
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
@ -54,8 +57,10 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DateContextMenu extends ContextMenu {
|
private static class DateContextMenu extends HashIndexEntryContextMenu {
|
||||||
public DateContextMenu(String date, BlockTransactionHashIndex reference) {
|
public DateContextMenu(TreeTableView<Entry> treeTableView, UtxoEntry utxoEntry, String date) {
|
||||||
|
super(treeTableView, utxoEntry);
|
||||||
|
|
||||||
MenuItem copyDate = new MenuItem("Copy Date");
|
MenuItem copyDate = new MenuItem("Copy Date");
|
||||||
copyDate.setOnAction(AE -> {
|
copyDate.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
|
|
@ -68,7 +73,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||||
copyHeight.setOnAction(AE -> {
|
copyHeight.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(reference.getHeight() > 0 ? Integer.toString(reference.getHeight()) : "Mempool");
|
content.putString(utxoEntry.getHashIndex().getHeight() > 0 ? Integer.toString(utxoEntry.getHashIndex().getHeight()) : "Mempool");
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
@ -79,7 +80,7 @@ public class DescriptorArea extends CodeArea {
|
||||||
copyOutputDescriptor.setOnAction(AE -> {
|
copyOutputDescriptor.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
|
content.putString(OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true));
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
getItems().add(copyOutputDescriptor);
|
getItems().add(copyOutputDescriptor);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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, BBQR bbqr, boolean selectBbqrButton) {
|
||||||
|
super(ur, bbqr, false, false, selectBbqrButton);
|
||||||
|
|
||||||
|
DialogPane dialogPane = getDialogPane();
|
||||||
|
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||||
|
dialogPane.getButtonTypes().add(pdfButtonType);
|
||||||
|
|
||||||
|
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
||||||
|
pdfButton.setGraphicTextGap(5);
|
||||||
|
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||||
|
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||||
|
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||||
|
|
||||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||||
|
|
||||||
List<Device> devices = AppServices.getDevices();
|
List<Device> devices = getDevices();
|
||||||
if(devices == null || devices.isEmpty()) {
|
if(devices == null || devices.isEmpty()) {
|
||||||
scanButton.setDefaultButton(true);
|
scanButton.setDefaultButton(true);
|
||||||
scanBox.setVisible(true);
|
scanBox.setVisible(true);
|
||||||
|
|
@ -91,11 +91,16 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||||
|
|
||||||
dialogPane.setPrefWidth(500);
|
dialogPane.setPrefWidth(500);
|
||||||
dialogPane.setPrefHeight(360);
|
dialogPane.setPrefHeight(360);
|
||||||
|
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<Device> getDevices() {
|
||||||
|
return AppServices.getDevices();
|
||||||
|
}
|
||||||
|
|
||||||
private void scan() {
|
private void scan() {
|
||||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private final OutputDescriptor outputDescriptor;
|
private final OutputDescriptor outputDescriptor;
|
||||||
|
|
||||||
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||||
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.outputDescriptor = outputDescriptor;
|
this.outputDescriptor = outputDescriptor;
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.DeviceAddressEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceGetAddressDialog extends DeviceDialog<Address> {
|
||||||
|
public DeviceGetAddressDialog(List<String> operationFingerprints) {
|
||||||
|
super(operationFingerprints);
|
||||||
|
EventManager.get().register(this);
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||||
|
return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void deviceAddress(DeviceAddressEvent event) {
|
||||||
|
setResult(event.getAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,36 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
import com.sparrowwallet.drongo.ExtendedKey;
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import com.sparrowwallet.drongo.policy.Policy;
|
import com.sparrowwallet.drongo.policy.Policy;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Device;
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
import com.sparrowwallet.sparrow.io.Hwi;
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.validation.ValidationResult;
|
import org.controlsfx.validation.ValidationResult;
|
||||||
import org.controlsfx.validation.ValidationSupport;
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
|
@ -35,8 +39,10 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class DevicePane extends TitledDescriptionPane {
|
public class DevicePane extends TitledDescriptionPane {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||||
|
|
@ -59,13 +65,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private Button displayAddressButton;
|
private Button displayAddressButton;
|
||||||
private Button signMessageButton;
|
private Button signMessageButton;
|
||||||
private Button discoverKeystoresButton;
|
private Button discoverKeystoresButton;
|
||||||
|
private ButtonBase getPrivateKeyButton;
|
||||||
|
private Button getAddressButton;
|
||||||
|
|
||||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||||
|
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||||
|
private final StringProperty messageProperty = new SimpleStringProperty("");
|
||||||
|
|
||||||
private boolean defaultDevice;
|
private boolean defaultDevice;
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
this.deviceOperation = DeviceOperation.IMPORT;
|
this.deviceOperation = DeviceOperation.IMPORT;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -84,13 +94,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
initialise(device);
|
initialise(device);
|
||||||
|
|
||||||
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
this.deviceOperation = DeviceOperation.SIGN;
|
this.deviceOperation = DeviceOperation.SIGN;
|
||||||
this.wallet = null;
|
this.wallet = wallet;
|
||||||
this.psbt = psbt;
|
this.psbt = psbt;
|
||||||
this.outputDescriptor = null;
|
this.outputDescriptor = null;
|
||||||
this.keyDerivation = null;
|
this.keyDerivation = null;
|
||||||
|
|
@ -107,11 +121,15 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
initialise(device);
|
initialise(device);
|
||||||
|
|
||||||
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
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.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -134,7 +152,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -153,11 +171,15 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
initialise(device);
|
initialise(device);
|
||||||
|
|
||||||
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
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.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.psbt = null;
|
this.psbt = null;
|
||||||
|
|
@ -179,6 +201,41 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||||
|
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||||
|
this.deviceOperation = deviceOperation;
|
||||||
|
this.wallet = null;
|
||||||
|
this.psbt = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.keyDerivation = null;
|
||||||
|
this.message = null;
|
||||||
|
this.device = device;
|
||||||
|
this.defaultDevice = defaultDevice;
|
||||||
|
this.availableAccounts = null;
|
||||||
|
|
||||||
|
setDefaultStatus();
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
|
||||||
|
ButtonBase button;
|
||||||
|
if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) {
|
||||||
|
createGetPrivateKeyButton();
|
||||||
|
button = getPrivateKeyButton;
|
||||||
|
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||||
|
createGetAddressButton();
|
||||||
|
button = getAddressButton;
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialise(device);
|
||||||
|
|
||||||
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonBox.getChildren().add(button);
|
||||||
|
}
|
||||||
|
|
||||||
private void initialise(Device device) {
|
private void initialise(Device device) {
|
||||||
if(device.isNeedsPinSent()) {
|
if(device.isNeedsPinSent()) {
|
||||||
unlockButton.setDefaultButton(defaultDevice);
|
unlockButton.setDefaultButton(defaultDevice);
|
||||||
|
|
@ -202,7 +259,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDefaultStatus() {
|
private void setDefaultStatus() {
|
||||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createUnlockButton() {
|
private void createUnlockButton() {
|
||||||
|
|
@ -240,7 +297,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
if(importButton instanceof SplitMenuButton importMenuButton) {
|
if(importButton instanceof SplitMenuButton importMenuButton) {
|
||||||
if(wallet.getScriptType() == null) {
|
if(wallet.getScriptType() == null) {
|
||||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
|
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
|
||||||
for(ScriptType scriptType : scriptTypes) {
|
for(ScriptType scriptType : scriptTypes) {
|
||||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||||
|
|
@ -322,6 +379,60 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
discoverKeystoresButton.setVisible(false);
|
discoverKeystoresButton.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createGetPrivateKeyButton() {
|
||||||
|
int currentSlot = 0;
|
||||||
|
boolean initialized = true;
|
||||||
|
try {
|
||||||
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), null);
|
||||||
|
currentSlot = cardApi.getCurrentSlot();
|
||||||
|
initialized = cardApi.isInitialized();
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrivateKeyButton = currentSlot > 0 ? new SplitMenuButton() : new Button();
|
||||||
|
getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getPrivateKeyButton.setText("Get Private Key");
|
||||||
|
getPrivateKeyButton.setOnAction(event -> {
|
||||||
|
getPrivateKeyButton.setDisable(true);
|
||||||
|
getPrivateKey(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(getPrivateKeyButton instanceof SplitMenuButton getPrivateKeyMenuButton) {
|
||||||
|
int[] previousSlots = IntStream.range(0, currentSlot).toArray();
|
||||||
|
for(int previousSlot : previousSlots) {
|
||||||
|
MenuItem previousSlotItem = new MenuItem("Slot #" + previousSlot);
|
||||||
|
previousSlotItem.setOnAction(event -> {
|
||||||
|
getPrivateKeyButton.setDisable(true);
|
||||||
|
getPrivateKey(previousSlot);
|
||||||
|
});
|
||||||
|
getPrivateKeyMenuButton.getItems().add(previousSlotItem);
|
||||||
|
}
|
||||||
|
if(initialized) {
|
||||||
|
int finalSlot = currentSlot;
|
||||||
|
MenuItem currentSlotItem = new MenuItem("Current Slot");
|
||||||
|
currentSlotItem.setOnAction(event -> {
|
||||||
|
getPrivateKeyButton.setDisable(true);
|
||||||
|
getPrivateKey(finalSlot);
|
||||||
|
});
|
||||||
|
getPrivateKeyMenuButton.getItems().add(currentSlotItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
|
||||||
|
getPrivateKeyButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGetAddressButton() {
|
||||||
|
getAddressButton = new Button("Get Address");
|
||||||
|
getAddressButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getAddressButton.setOnAction(event -> {
|
||||||
|
getAddressButton.setDisable(true);
|
||||||
|
getAddress();
|
||||||
|
});
|
||||||
|
getAddressButton.managedProperty().bind(getAddressButton.visibleProperty());
|
||||||
|
getAddressButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void unlock(Device device) {
|
private void unlock(Device device) {
|
||||||
if(device.getModel().requiresPinPrompt()) {
|
if(device.getModel().requiresPinPrompt()) {
|
||||||
promptPin();
|
promptPin();
|
||||||
|
|
@ -332,7 +443,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
VBox vBox = new VBox();
|
VBox vBox = new VBox();
|
||||||
vBox.setMaxHeight(120);
|
vBox.setMaxHeight(120);
|
||||||
vBox.setSpacing(42);
|
vBox.setSpacing(42);
|
||||||
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
pinField = new ViewPasswordField();
|
||||||
Platform.runLater(() -> pinField.requestFocus());
|
Platform.runLater(() -> pinField.requestFocus());
|
||||||
enterPinButton = new Button("Enter PIN");
|
enterPinButton = new Button("Enter PIN");
|
||||||
enterPinButton.setDefaultButton(true);
|
enterPinButton.setDefaultButton(true);
|
||||||
|
|
@ -342,20 +453,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
});
|
});
|
||||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||||
|
|
||||||
TilePane tilePane = new TilePane();
|
GridPane gridPane = new GridPane();
|
||||||
tilePane.setPrefColumns(3);
|
gridPane.setHgap(10);
|
||||||
tilePane.setHgap(10);
|
gridPane.setVgap(10);
|
||||||
tilePane.setVgap(10);
|
gridPane.setMaxWidth(150);
|
||||||
tilePane.setMaxWidth(150);
|
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||||
tilePane.setMaxHeight(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++) {
|
for(int i = 0; i < digits.length; i++) {
|
||||||
Button pinButton = new Button();
|
Button pinButton = new Button();
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||||
pinButton.setGraphic(circle);
|
pinButton.setGraphic(circle);
|
||||||
pinButton.setUserData(digits[i]);
|
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 -> {
|
pinButton.setOnAction(event -> {
|
||||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||||
});
|
});
|
||||||
|
|
@ -363,7 +480,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
|
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setSpacing(50);
|
contentBox.setSpacing(50);
|
||||||
contentBox.getChildren().add(tilePane);
|
contentBox.getChildren().add(gridPane);
|
||||||
contentBox.getChildren().add(vBox);
|
contentBox.getChildren().add(vBox);
|
||||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||||
|
|
@ -372,7 +489,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node getPassphraseEntry() {
|
private Node getPassphraseEntry() {
|
||||||
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
CustomPasswordField passphraseField = new ViewPasswordField();
|
||||||
passphrase.bind(passphraseField.textProperty());
|
passphrase.bind(passphraseField.textProperty());
|
||||||
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||||
passphraseField.setOnAction(event -> {
|
passphraseField.setOnAction(event -> {
|
||||||
|
|
@ -555,7 +672,36 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void importKeystore(List<ChildNumber> derivation) {
|
private void importKeystore(List<ChildNumber> derivation) {
|
||||||
if(device.getFingerprint() == null) {
|
if(device.isCard()) {
|
||||||
|
try {
|
||||||
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
|
if(!cardApi.isInitialized()) {
|
||||||
|
if(pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
|
setDescription(pin.get().isEmpty() ? (device.getModel().hasDefaultPin() ? "Enter PIN code" : "Choose a PIN code") : "PIN code too short");
|
||||||
|
setContent(getCardPinEntry(importButton));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
importButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDescription("Card not initialized");
|
||||||
|
setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
|
||||||
|
handleCardOperation(importService, importButton, "Import", true, event -> {
|
||||||
|
importKeystore(derivation, importService.getValue());
|
||||||
|
});
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Import Error: " + e.getMessage(), e);
|
||||||
|
setError("Import Error", e.getMessage());
|
||||||
|
importButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else if(device.getFingerprint() == null) {
|
||||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||||
List<Device> devices = enumerateService.getValue();
|
List<Device> devices = enumerateService.getValue();
|
||||||
|
|
@ -592,18 +738,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||||
|
|
||||||
if(wallet.getScriptType() == null) {
|
importKeystore(derivation, keystore);
|
||||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
|
||||||
wallet.setName(device.getModel().toDisplayString());
|
|
||||||
wallet.setPolicyType(PolicyType.SINGLE);
|
|
||||||
wallet.setScriptType(scriptType);
|
|
||||||
wallet.getKeystores().add(keystore);
|
|
||||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
|
||||||
|
|
||||||
EventManager.get().post(new WalletImportEvent(wallet));
|
|
||||||
} else {
|
|
||||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
setError("Could not retrieve xpub", e.getMessage());
|
setError("Could not retrieve xpub", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
@ -617,27 +752,85 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
getXpubService.start();
|
getXpubService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
|
||||||
|
if(wallet.getScriptType() == null) {
|
||||||
|
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||||
|
wallet.setName(device.getModel().toDisplayString());
|
||||||
|
wallet.setPolicyType(PolicyType.SINGLE);
|
||||||
|
wallet.setScriptType(scriptType);
|
||||||
|
wallet.getKeystores().add(keystore);
|
||||||
|
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||||
|
|
||||||
|
EventManager.get().post(new WalletImportEvent(wallet));
|
||||||
|
} else {
|
||||||
|
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sign() {
|
private void sign() {
|
||||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
if(device.isCard()) {
|
||||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
try {
|
||||||
PSBT signedPsbt = signPSBTService.getValue();
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
|
||||||
|
handleCardOperation(signService, signButton, "Signing", true, event -> {
|
||||||
|
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
|
||||||
|
});
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Signing Error: " + e.getMessage(), e);
|
||||||
|
setError("Signing Error", e.getMessage());
|
||||||
|
signButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
|
||||||
|
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());
|
||||||
|
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||||
|
signButton.setDisable(false);
|
||||||
|
});
|
||||||
|
setDescription("Signing...");
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
signPSBTService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
|
||||||
|
if(pinRequired && pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
|
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||||
|
setContent(getCardPinEntry(operationButton));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
operationButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.setOnSucceeded(successHandler);
|
||||||
|
service.setOnFailed(event -> {
|
||||||
|
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||||
|
if(rootCause instanceof CardAuthorizationException) {
|
||||||
|
setError(rootCause.getMessage(), null);
|
||||||
|
setContent(getCardPinEntry(operationButton));
|
||||||
|
} else {
|
||||||
|
log.error(operationDescription + " Error: " + rootCause.getMessage(), event.getSource().getException());
|
||||||
|
setError(operationDescription + " Error", rootCause.getMessage());
|
||||||
|
}
|
||||||
|
operationButton.setDisable(false);
|
||||||
});
|
});
|
||||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
service.start();
|
||||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
|
||||||
log.error("Signing Error: " + signPSBTService.getException().getMessage());
|
|
||||||
signButton.setDisable(false);
|
|
||||||
});
|
|
||||||
setDescription("Signing...");
|
|
||||||
showHideLink.setVisible(false);
|
|
||||||
signPSBTService.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayAddress() {
|
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 -> {
|
displayAddressService.setOnSucceeded(successEvent -> {
|
||||||
String address = displayAddressService.getValue();
|
String address = displayAddressService.getValue();
|
||||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||||
|
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
|
||||||
});
|
});
|
||||||
displayAddressService.setOnFailed(failedEvent -> {
|
displayAddressService.setOnFailed(failedEvent -> {
|
||||||
setError("Could not display address", displayAddressService.getException().getMessage());
|
setError("Could not display address", displayAddressService.getException().getMessage());
|
||||||
|
|
@ -647,18 +840,53 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
displayAddressService.start();
|
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() {
|
private void signMessage() {
|
||||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
if(device.isCard()) {
|
||||||
signMessageService.setOnSucceeded(successEvent -> {
|
try {
|
||||||
String signature = signMessageService.getValue();
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
|
||||||
});
|
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
|
||||||
signMessageService.setOnFailed(failedEvent -> {
|
String signature = signMessageService.getValue();
|
||||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||||
signMessageButton.setDisable(false);
|
});
|
||||||
});
|
} catch(Exception e) {
|
||||||
setDescription("Signing message...");
|
log.error("Signing Error: " + e.getMessage(), e);
|
||||||
signMessageService.start();
|
setError("Signing Error", e.getMessage());
|
||||||
|
signButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||||
|
signMessageService.setOnSucceeded(successEvent -> {
|
||||||
|
String signature = signMessageService.getValue();
|
||||||
|
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||||
|
});
|
||||||
|
signMessageService.setOnFailed(failedEvent -> {
|
||||||
|
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||||
|
signMessageButton.setDisable(false);
|
||||||
|
});
|
||||||
|
setDescription("Signing message...");
|
||||||
|
signMessageService.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void discoverKeystores() {
|
private void discoverKeystores() {
|
||||||
|
|
@ -697,17 +925,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
|
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores);
|
||||||
walletDiscoveryService.setOnSucceeded(event -> {
|
accountDiscoveryService.setOnSucceeded(event -> {
|
||||||
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
|
importedKeystores.keySet().retainAll(accountDiscoveryService.getValue());
|
||||||
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
||||||
});
|
});
|
||||||
walletDiscoveryService.setOnFailed(event -> {
|
accountDiscoveryService.setOnFailed(event -> {
|
||||||
log.error("Failed to discover accounts", event.getSource().getException());
|
log.error("Failed to discover accounts", event.getSource().getException());
|
||||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||||
discoverKeystoresButton.setDisable(false);
|
discoverKeystoresButton.setDisable(false);
|
||||||
});
|
});
|
||||||
walletDiscoveryService.start();
|
accountDiscoveryService.start();
|
||||||
});
|
});
|
||||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||||
|
|
@ -718,6 +946,55 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
getXpubsService.start();
|
getXpubsService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void getPrivateKey(Integer slot) {
|
||||||
|
if(device.isCard()) {
|
||||||
|
try {
|
||||||
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
|
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(slot, messageProperty);
|
||||||
|
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
|
||||||
|
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
|
||||||
|
});
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Private Key Error: " + e.getMessage(), e);
|
||||||
|
setError("Private Key Error", e.getMessage());
|
||||||
|
getPrivateKeyButton.setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getAddress() {
|
||||||
|
if(device.isCard()) {
|
||||||
|
try {
|
||||||
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
|
if(!cardApi.isInitialized()) {
|
||||||
|
if(pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
|
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||||
|
setContent(getCardPinEntry(getAddressButton));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
getAddressButton.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDescription("Card not initialized");
|
||||||
|
setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS));
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
setExpanded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Service<Address> addressService = cardApi.getAddressService(messageProperty);
|
||||||
|
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
|
||||||
|
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
|
||||||
|
});
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Address Error: " + e.getMessage(), e);
|
||||||
|
setError("Address Error", e.getMessage());
|
||||||
|
getAddressButton.setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showOperationButton() {
|
private void showOperationButton() {
|
||||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||||
if(defaultDevice) {
|
if(defaultDevice) {
|
||||||
|
|
@ -725,7 +1002,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
importButton.setVisible(true);
|
importButton.setVisible(true);
|
||||||
showHideLink.setText("Show derivation...");
|
showHideLink.setText("Show derivation...");
|
||||||
showHideLink.setVisible(true);
|
showHideLink.setVisible(!device.isCard());
|
||||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||||
|
|
@ -744,6 +1021,16 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||||
discoverKeystoresButton.setVisible(true);
|
discoverKeystoresButton.setVisible(true);
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
|
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
|
||||||
|
if(defaultDevice) {
|
||||||
|
getPrivateKeyButton.getStyleClass().add("default-button");
|
||||||
|
}
|
||||||
|
getPrivateKeyButton.setVisible(true);
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
|
||||||
|
getAddressButton.setDefaultButton(defaultDevice);
|
||||||
|
getAddressButton.setVisible(true);
|
||||||
|
showHideLink.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -751,7 +1038,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
TextField derivationField = new TextField();
|
TextField derivationField = new TextField();
|
||||||
derivationField.setPromptText("Derivation path");
|
derivationField.setPromptText("Derivation path");
|
||||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||||
derivationField.setDisable(keyDerivation != null);
|
derivationField.setDisable(device.isCard() || keyDerivation != null);
|
||||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||||
|
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
|
@ -786,11 +1073,161 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
|
if(device.getModel().requiresSeedInitialization()) {
|
||||||
|
return getCardSeedInitializationPanel(cardApi, operationButton, deviceOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCardEntropyInitializationPanel(cardApi, operationButton, deviceOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getCardSeedInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
|
VBox confirmationBox = new VBox(5);
|
||||||
|
CustomPasswordField confirmationPin = new ViewPasswordField();
|
||||||
|
confirmationPin.setPromptText("Re-enter chosen PIN");
|
||||||
|
confirmationBox.getChildren().add(confirmationPin);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
if(!pin.get().equals(confirmationPin.getText())) {
|
||||||
|
setError("PIN Error", "The confirmation PIN did not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pinSize = pin.get().length();
|
||||||
|
if(pinSize < device.getModel().getMinPinLength() || pinSize > device.getModel().getMaxPinLength()) {
|
||||||
|
setError("PIN Error", "PIN length must be between " + device.getModel().getMinPinLength() + " and " + device.getModel().getMaxPinLength() + " characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(device.getModel().toDisplayString() + " Seed Words", 12);
|
||||||
|
seedEntryDialog.initOwner(this.getScene().getWindow());
|
||||||
|
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||||
|
if(optWords.isPresent()) {
|
||||||
|
try {
|
||||||
|
List<String> mnemonicWords = optWords.get();
|
||||||
|
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
|
||||||
|
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, "", System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||||
|
byte[] seedBytes = seed.getSeedBytes();
|
||||||
|
|
||||||
|
Service<Void> cardInitializationService = cardApi.getInitializationService(seedBytes, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
operationButton.setDisable(false);
|
||||||
|
setDefaultStatus();
|
||||||
|
setExpanded(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
log.error("Invalid seed entered", e);
|
||||||
|
AppServices.showErrorDialog("Invalid seed entered", "The seed was invalid.\n\n" + e.getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(confirmationBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(confirmationBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getCardEntropyInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
|
VBox initTypeBox = new VBox(5);
|
||||||
|
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||||
|
RadioButton advanced = new RadioButton("Advanced");
|
||||||
|
TextField entropy = new TextField();
|
||||||
|
entropy.setPromptText("Enter input for user entropy");
|
||||||
|
entropy.setDisable(true);
|
||||||
|
|
||||||
|
ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
automatic.setToggleGroup(toggleGroup);
|
||||||
|
advanced.setToggleGroup(toggleGroup);
|
||||||
|
automatic.setSelected(true);
|
||||||
|
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
entropy.setDisable(newValue == automatic);
|
||||||
|
});
|
||||||
|
|
||||||
|
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||||
|
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
if(deviceOperation == DeviceOperation.IMPORT) {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||||
|
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
|
||||||
|
}
|
||||||
|
operationButton.setDisable(false);
|
||||||
|
setDefaultStatus();
|
||||||
|
setExpanded(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||||
|
if(rootCause instanceof CardAuthorizationException) {
|
||||||
|
setError(rootCause.getMessage(), null);
|
||||||
|
setContent(getCardPinEntry(operationButton));
|
||||||
|
operationButton.setDisable(false);
|
||||||
|
} else {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getCardPinEntry(ButtonBase operationButton) {
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
|
||||||
|
CustomPasswordField pinField = new ViewPasswordField();
|
||||||
|
pinField.setPromptText("PIN Code");
|
||||||
|
if(operationButton instanceof Button defaultButton) {
|
||||||
|
defaultButton.setDefaultButton(true);
|
||||||
|
}
|
||||||
|
pin.bind(pinField.textProperty());
|
||||||
|
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||||
|
Platform.runLater(pinField::requestFocus);
|
||||||
|
|
||||||
|
HBox contentBox = new HBox();
|
||||||
|
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||||
|
contentBox.setSpacing(20);
|
||||||
|
contentBox.getChildren().add(pinField);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
contentBox.setPrefHeight(50);
|
||||||
|
|
||||||
|
vBox.getChildren().add(contentBox);
|
||||||
|
|
||||||
|
return vBox;
|
||||||
|
}
|
||||||
|
|
||||||
public Device getDevice() {
|
public Device getDevice() {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DeviceOperation {
|
public enum DeviceOperation {
|
||||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,24 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||||
import com.sparrowwallet.sparrow.io.Device;
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
|
||||||
|
|
||||||
|
private final Wallet wallet;
|
||||||
private final PSBT psbt;
|
private final PSBT psbt;
|
||||||
|
|
||||||
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
|
public DeviceSignDialog(Wallet wallet, List<String> operationFingerprints, PSBT psbt) {
|
||||||
super(operationFingerprints);
|
super(operationFingerprints);
|
||||||
|
this.wallet = wallet;
|
||||||
this.psbt = psbt;
|
this.psbt = psbt;
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
setOnCloseRequest(event -> {
|
setOnCloseRequest(event -> {
|
||||||
|
|
@ -23,7 +30,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||||
return new DevicePane(psbt, device, defaultDevice);
|
return new DevicePane(wallet, psbt, device, defaultDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
|
||||||
|
public DeviceUnsealDialog(List<String> operationFingerprints) {
|
||||||
|
super(operationFingerprints);
|
||||||
|
EventManager.get().register(this);
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||||
|
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
|
||||||
|
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
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() {
|
||||||
|
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,777 @@
|
||||||
|
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_PREFIX = "sparrow_";
|
||||||
|
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) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
|
||||||
|
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) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && 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,24 +1,28 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.wallet.*;
|
import com.sparrowwallet.sparrow.wallet.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.FontAwesome;
|
import org.controlsfx.glyphfont.FontAwesome;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -31,11 +35,15 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
public class EntryCell extends TreeTableCell<Entry, Entry> implements ConfirmationsListener {
|
||||||
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
||||||
|
|
||||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||||
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
|
public static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*?)( \\(Replaced By Fee( #)?(\\d+)?\\)).*?");
|
||||||
|
|
||||||
|
private static EntryCell lastCell;
|
||||||
|
|
||||||
|
private IntegerProperty confirmationsProperty;
|
||||||
|
|
||||||
public EntryCell() {
|
public EntryCell() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -48,14 +56,19 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
protected void updateItem(Entry entry, boolean empty) {
|
protected void updateItem(Entry entry, boolean empty) {
|
||||||
super.updateItem(entry, empty);
|
super.updateItem(entry, empty);
|
||||||
|
|
||||||
|
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||||
|
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastCell = this;
|
||||||
|
|
||||||
applyRowStyles(this, entry);
|
applyRowStyles(this, entry);
|
||||||
|
|
||||||
if(empty) {
|
if(empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
} else {
|
} else {
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry transactionEntry) {
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
|
||||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
setText("Unconfirmed Parent");
|
setText("Unconfirmed Parent");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
|
|
@ -69,10 +82,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
|
tooltip.setText(getTooltip(transactionEntry));
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
||||||
|
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||||
|
tooltip.setOnShowing(event -> {
|
||||||
|
tooltip.setText(getTooltip(transactionEntry));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
HBox actionBox = new HBox();
|
HBox actionBox = new HBox();
|
||||||
|
actionBox.getStyleClass().add("cell-actions");
|
||||||
Button viewTransactionButton = new Button("");
|
Button viewTransactionButton = new Button("");
|
||||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||||
viewTransactionButton.setOnAction(event -> {
|
viewTransactionButton.setOnAction(event -> {
|
||||||
|
|
@ -81,11 +102,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
actionBox.getChildren().add(viewTransactionButton);
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
|
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||||
|
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
Button increaseFeeButton = new Button("");
|
Button increaseFeeButton = new Button("");
|
||||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFeeButton.setOnAction(event -> {
|
increaseFeeButton.setOnAction(event -> {
|
||||||
increaseFee(transactionEntry);
|
increaseFee(transactionEntry, false);
|
||||||
});
|
});
|
||||||
actionBox.getChildren().add(increaseFeeButton);
|
actionBox.getChildren().add(increaseFeeButton);
|
||||||
}
|
}
|
||||||
|
|
@ -100,30 +122,35 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
setText(address.toString());
|
setText(address.toString());
|
||||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
tooltip.setText(nodeEntry.getNode().toString());
|
tooltip.setText(nodeEntry.getNode().toString());
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
getStyleClass().add("address-cell");
|
getStyleClass().add("address-cell");
|
||||||
|
|
||||||
HBox actionBox = new HBox();
|
HBox actionBox = new HBox();
|
||||||
Button receiveButton = new Button("");
|
actionBox.getStyleClass().add("cell-actions");
|
||||||
receiveButton.setGraphic(getReceiveGlyph());
|
|
||||||
receiveButton.setOnAction(event -> {
|
|
||||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
|
||||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
|
||||||
});
|
|
||||||
actionBox.getChildren().add(receiveButton);
|
|
||||||
|
|
||||||
if(canSignMessage(nodeEntry.getWallet())) {
|
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||||
|
Button receiveButton = new Button("");
|
||||||
|
receiveButton.setGraphic(getReceiveGlyph());
|
||||||
|
receiveButton.setOnAction(event -> {
|
||||||
|
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||||
|
});
|
||||||
|
actionBox.getChildren().add(receiveButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(canSignMessage(nodeEntry.getNode())) {
|
||||||
Button signMessageButton = new Button("");
|
Button signMessageButton = new Button("");
|
||||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||||
signMessageButton.setOnAction(event -> {
|
signMessageButton.setOnAction(event -> {
|
||||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||||
|
messageSignDialog.initOwner(getTreeTableView().getScene().getWindow());
|
||||||
messageSignDialog.showAndWait();
|
messageSignDialog.showAndWait();
|
||||||
});
|
});
|
||||||
actionBox.getChildren().add(signMessageButton);
|
actionBox.getChildren().add(signMessageButton);
|
||||||
|
|
@ -136,15 +163,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
setGraphic(new HBox());
|
setGraphic(new HBox());
|
||||||
}
|
}
|
||||||
} else if(entry instanceof HashIndexEntry) {
|
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
|
||||||
setText(hashIndexEntry.getDescription());
|
setText(hashIndexEntry.getDescription());
|
||||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
|
tooltip.setShowDelay(Duration.millis(250));
|
||||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
||||||
HBox actionBox = new HBox();
|
HBox actionBox = new HBox();
|
||||||
|
actionBox.getStyleClass().add("cell-actions");
|
||||||
Button viewTransactionButton = new Button("");
|
Button viewTransactionButton = new Button("");
|
||||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||||
viewTransactionButton.setOnAction(event -> {
|
viewTransactionButton.setOnAction(event -> {
|
||||||
|
|
@ -161,84 +189,160 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
actionBox.getChildren().add(spendUtxoButton);
|
actionBox.getChildren().add(spendUtxoButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void increaseFee(TransactionEntry transactionEntry) {
|
@Override
|
||||||
|
public IntegerProperty getConfirmationsProperty() {
|
||||||
|
if(confirmationsProperty == null) {
|
||||||
|
confirmationsProperty = new SimpleIntegerProperty();
|
||||||
|
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||||
|
getStyleClass().remove("confirming");
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmationsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
|
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.filter(TransactionInput::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())
|
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if(utxos.isEmpty()) {
|
||||||
|
log.error("No UTXOs to replace");
|
||||||
|
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction - no replaceable UTXOs were found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
|
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.map(e -> (HashIndexEntry)e)
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
|
||||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
.map(e -> blockTransaction.getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
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();
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
double vSize = tx.getVirtualSize();
|
double vSize = tx.getVirtualSize();
|
||||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
if(changeTotal == 0) {
|
||||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
//Add change output length to vSize if change was not present on the original transaction
|
||||||
Collections.shuffle(walletUtxos);
|
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
|
||||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
vSize += changeOutput.getLength();
|
||||||
//If there is insufficent change output, include another random UTXO so the fee can be increased
|
|
||||||
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
|
||||||
utxos.add(utxo);
|
|
||||||
changeTotal += utxo.getValue();
|
|
||||||
vSize += inputSize;
|
|
||||||
}
|
}
|
||||||
|
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||||
|
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(utxos), new SpentTxoFilter(blockTransaction.getHash()), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
|
||||||
|
double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate();
|
||||||
|
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 && 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()) {
|
||||||
|
utxos.add(utxo);
|
||||||
|
changeTotal += utxo.getValue();
|
||||||
|
vSize += inputSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Long fee = blockTransaction.getFee();
|
||||||
|
if(fee != null) {
|
||||||
|
//Replacement tx fees must be greater than the original tx fees by its minimum relay cost
|
||||||
|
fee += (long)Math.ceil(vSize * AppServices.getMinimumRelayFeeRate());
|
||||||
|
}
|
||||||
|
Long rbfFee = fee;
|
||||||
|
|
||||||
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||||
externalOutputs.removeAll(ourOutputs);
|
externalOutputs.removeAll(ourOutputs);
|
||||||
externalOutputs.addAll(consolidationOutputs);
|
externalOutputs.addAll(consolidationOutputs);
|
||||||
|
final long rbfChange = changeTotal;
|
||||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||||
try {
|
try {
|
||||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||||
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(label);
|
label = REPLACED_BY_FEE_SUFFIX.matcher(label).replaceAll("$1");
|
||||||
|
String[] paymentLabels = label.split(", ");
|
||||||
|
if(externalOutputs.size() > 1 && externalOutputs.size() == paymentLabels.length) {
|
||||||
|
label = paymentLabels[externalOutputs.indexOf(txOutput)];
|
||||||
|
}
|
||||||
|
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel());
|
||||||
if(matcher.matches()) {
|
if(matcher.matches()) {
|
||||||
String base = matcher.group(1);
|
if(matcher.groupCount() > 3 && matcher.group(4) != null) {
|
||||||
if(matcher.groupCount() > 2 && matcher.group(3) != null) {
|
int count = Integer.parseInt(matcher.group(4)) + 1;
|
||||||
int count = Integer.parseInt(matcher.group(3)) + 1;
|
label += " (Replaced By Fee #" + count + ")";
|
||||||
label = base + "(Replaced By Fee #" + count + ")";
|
|
||||||
} else {
|
} else {
|
||||||
label = base + "(Replaced By Fee #2)";
|
label += " (Replaced By Fee #2)";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
|
label += " (Replaced By Fee)";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
|
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
|
||||||
|
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;
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.error("Error creating RBF payment", e);
|
log.error("Error creating RBF payment", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<byte[]> opReturns = externalOutputs.stream().map(txOutput -> {
|
||||||
|
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
|
||||||
|
if(scriptChunks.size() != 2 || scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(scriptChunks.get(1).getData() != null) {
|
||||||
|
return scriptChunks.get(1).getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
|
||||||
if(payments.isEmpty()) {
|
if(payments.isEmpty()) {
|
||||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(cancelTransaction) {
|
||||||
|
Payment existing = payments.get(0);
|
||||||
|
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
|
||||||
|
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
|
||||||
|
payments.clear();
|
||||||
|
payments.add(payment);
|
||||||
|
opReturns.clear();
|
||||||
|
}
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double getMaxFeeRate() {
|
private static Double getMaxFeeRate() {
|
||||||
|
|
@ -254,28 +358,57 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
||||||
.filter(e -> e instanceof HashIndexEntry)
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
.map(e -> (HashIndexEntry)e)
|
.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)
|
.map(HashIndexEntry::getHashIndex)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if(ourOutputs.isEmpty()) {
|
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 utxo = ourOutputs.get(0);
|
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();
|
||||||
|
double vSize = inputSize + txOutput.getLength();
|
||||||
|
|
||||||
|
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
|
||||||
|
double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate();
|
||||||
|
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);
|
||||||
|
|
||||||
|
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||||
|
utxos.add(cpfpUtxo);
|
||||||
|
long inputTotal = cpfpUtxo.getValue();
|
||||||
|
while((inputTotal - (long)(getMaxFeeRate() * vSize)) < dustThreshold && !outputGroups.isEmpty()) {
|
||||||
|
//If there is insufficient input value, include another random output group so the fee can be increased
|
||||||
|
OutputGroup outputGroup = outputGroups.remove(0);
|
||||||
|
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||||
|
utxos.add(utxo);
|
||||||
|
inputTotal += utxo.getValue();
|
||||||
|
vSize += inputSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
|
|
||||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||||
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
|
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canSignMessage(Wallet wallet) {
|
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
|
}
|
||||||
|
|
||||||
|
private static boolean canSignMessage(WalletNode walletNode) {
|
||||||
|
Wallet wallet = walletNode.getWallet();
|
||||||
|
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||||
|
|
@ -326,6 +459,34 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getTooltip(TransactionEntry transactionEntry) {
|
||||||
|
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
|
||||||
|
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||||
|
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
||||||
|
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
||||||
|
if(feeRate != null && vSizefromTip != null) {
|
||||||
|
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
|
||||||
|
|
||||||
|
String amount = vSizefromTip + " vB";
|
||||||
|
if(vSizefromTip > 1000 * 1000) {
|
||||||
|
amount = String.format("%.2f", (double)vSizefromTip / (1000 * 1000)) + " MvB";
|
||||||
|
} else if(vSizefromTip > 1000) {
|
||||||
|
amount = String.format("%.2f", (double)vSizefromTip / 1000) + " kvB";
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip += "\nConfirms in: " + (blocksFromTip > 1 ? blocksFromTip + "+ blocks" : "1 block") + " (" + amount + " from tip)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(feeRate != null) {
|
||||||
|
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
private static Glyph getViewTransactionGlyph() {
|
private static Glyph getViewTransactionGlyph() {
|
||||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||||
searchGlyph.setFontSize(12);
|
searchGlyph.setFontSize(12);
|
||||||
|
|
@ -338,6 +499,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
return increaseFeeGlyph;
|
return increaseFeeGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Glyph getCancelTransactionRBFGlyph() {
|
||||||
|
Glyph cancelTxGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BAN);
|
||||||
|
cancelTxGlyph.setFontSize(12);
|
||||||
|
return cancelTxGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
private static Glyph getIncreaseFeeCPFPGlyph() {
|
private static Glyph getIncreaseFeeCPFPGlyph() {
|
||||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||||
cpfpGlyph.setFontSize(12);
|
cpfpGlyph.setFontSize(12);
|
||||||
|
|
@ -382,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
|
|
||||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||||
|
Wallet wallet = transactionEntry.getWallet();
|
||||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -391,17 +559,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
});
|
});
|
||||||
getItems().add(viewTransaction);
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
increaseFee(transactionEntry);
|
increaseFee(transactionEntry, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 -> {
|
||||||
|
hide();
|
||||||
|
increaseFee(transactionEntry, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().add(cancelTx);
|
||||||
|
}
|
||||||
|
|
||||||
if(containsWalletOutputs(transactionEntry)) {
|
if(containsWalletOutputs(transactionEntry)) {
|
||||||
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||||
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||||
|
|
@ -413,6 +592,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
getItems().add(createCpfp);
|
getItems().add(createCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
|
|
@ -425,7 +613,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TransactionContextMenu extends ContextMenu {
|
protected static class TransactionContextMenu extends ContextMenu {
|
||||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -433,6 +621,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
hide();
|
hide();
|
||||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||||
});
|
});
|
||||||
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
|
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");
|
MenuItem copyDate = new MenuItem("Copy Date");
|
||||||
copyDate.setOnAction(AE -> {
|
copyDate.setOnAction(AE -> {
|
||||||
|
|
@ -441,6 +639,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
content.putString(date);
|
content.putString(date);
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyDate);
|
||||||
|
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
|
|
@ -449,6 +648,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
content.putString(blockTransaction.getHashAsString());
|
content.putString(blockTransaction.getHashAsString());
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyTxid);
|
||||||
|
|
||||||
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
||||||
copyHeight.setOnAction(AE -> {
|
copyHeight.setOnAction(AE -> {
|
||||||
|
|
@ -457,33 +657,71 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyHeight);
|
||||||
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AddressContextMenu extends ContextMenu {
|
public static class AddressContextMenu extends ContextMenu {
|
||||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
|
||||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||||
receiveToAddress.setOnAction(event -> {
|
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||||
hide();
|
receiveToAddress.setOnAction(event -> {
|
||||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
hide();
|
||||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||||
});
|
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||||
getItems().add(receiveToAddress);
|
});
|
||||||
|
getItems().add(receiveToAddress);
|
||||||
|
}
|
||||||
|
|
||||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
|
||||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||||
signVerifyMessage.setOnAction(AE -> {
|
signVerifyMessage.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||||
|
messageSignDialog.initOwner(treetable.getScene().getWindow());
|
||||||
messageSignDialog.showAndWait();
|
messageSignDialog.showAndWait();
|
||||||
});
|
});
|
||||||
getItems().add(signVerifyMessage);
|
getItems().add(signVerifyMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(addUtxoItems && nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
|
||||||
|
List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
|
||||||
|
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
|
||||||
|
spendUtxos.setGraphic(getSendGlyph());
|
||||||
|
spendUtxos.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
EventManager.get().post(new SendActionEvent(nodeEntry.getWallet(), utxos));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(nodeEntry.getWallet(), utxos)));
|
||||||
|
});
|
||||||
|
getItems().add(spendUtxos);
|
||||||
|
|
||||||
|
List<BlockTransactionHashIndex> unfrozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
|
||||||
|
if(!unfrozenUtxos.isEmpty()) {
|
||||||
|
MenuItem freezeUtxos = new MenuItem("Freeze UTXOs");
|
||||||
|
freezeUtxos.setGraphic(getFreezeGlyph());
|
||||||
|
freezeUtxos.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
unfrozenUtxos.forEach(utxo -> utxo.setStatus(Status.FROZEN));
|
||||||
|
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), unfrozenUtxos));
|
||||||
|
});
|
||||||
|
getItems().add(freezeUtxos);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BlockTransactionHashIndex> frozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
|
||||||
|
if(!frozenUtxos.isEmpty()) {
|
||||||
|
MenuItem unfreezeUtxos = new MenuItem("Unfreeze UTXOs");
|
||||||
|
unfreezeUtxos.setGraphic(getUnfreezeGlyph());
|
||||||
|
unfreezeUtxos.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
frozenUtxos.forEach(utxo -> utxo.setStatus(null));
|
||||||
|
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), frozenUtxos));
|
||||||
|
});
|
||||||
|
getItems().add(unfreezeUtxos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||||
copyAddress.setOnAction(AE -> {
|
copyAddress.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
|
|
@ -492,14 +730,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
|
||||||
copyHex.setOnAction(AE -> {
|
|
||||||
hide();
|
|
||||||
ClipboardContent content = new ClipboardContent();
|
|
||||||
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
|
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
|
||||||
});
|
|
||||||
|
|
||||||
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
|
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
|
||||||
copyOutputDescriptor.setOnAction(AE -> {
|
copyOutputDescriptor.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
|
|
@ -508,11 +738,23 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
getItems().addAll(copyAddress, copyOutputDescriptor);
|
||||||
|
|
||||||
|
if(nodeEntry != null) {
|
||||||
|
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
||||||
|
copyHex.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
Script outputScript = nodeEntry.getWallet().getOutputScript(nodeEntry.getNode());
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(Utils.bytesToHex(outputScript.getProgram()));
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
getItems().add(copyHex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class HashIndexEntryContextMenu extends ContextMenu {
|
static class HashIndexEntryContextMenu extends ContextMenu {
|
||||||
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||||
|
|
@ -568,40 +810,57 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
cell.getStyleClass().remove("transaction-row");
|
cell.getStyleClass().remove("transaction-row");
|
||||||
cell.getStyleClass().remove("node-row");
|
cell.getStyleClass().remove("node-row");
|
||||||
cell.getStyleClass().remove("utxo-row");
|
cell.getStyleClass().remove("utxo-row");
|
||||||
cell.getStyleClass().remove("address-cell");
|
cell.getStyleClass().remove("unconfirmed-row");
|
||||||
|
cell.getStyleClass().remove("summary-row");
|
||||||
|
boolean addressCell = cell.getStyleClass().remove("address-cell");
|
||||||
cell.getStyleClass().remove("hashindex-row");
|
cell.getStyleClass().remove("hashindex-row");
|
||||||
cell.getStyleClass().remove("confirming");
|
cell.getStyleClass().remove("confirming");
|
||||||
cell.getStyleClass().remove("negative-amount");
|
cell.getStyleClass().remove("negative-amount");
|
||||||
cell.getStyleClass().remove("spent");
|
cell.getStyleClass().remove("spent");
|
||||||
cell.getStyleClass().remove("unspendable");
|
cell.getStyleClass().remove("unspendable");
|
||||||
|
cell.getStyleClass().remove("number-field");
|
||||||
|
|
||||||
if(entry != null) {
|
if(entry != null) {
|
||||||
if(entry instanceof TransactionEntry) {
|
if(entry instanceof TransactionEntry transactionEntry) {
|
||||||
cell.getStyleClass().add("transaction-row");
|
cell.getStyleClass().add("transaction-row");
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||||
if(transactionEntry.isConfirming()) {
|
if(transactionEntry.isConfirming()) {
|
||||||
cell.getStyleClass().add("confirming");
|
cell.getStyleClass().add("confirming");
|
||||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
confirmationsListener.getConfirmationsProperty().bind(transactionEntry.confirmationsProperty());
|
||||||
if(!transactionEntry.isConfirming()) {
|
} else {
|
||||||
cell.getStyleClass().remove("confirming");
|
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) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
cell.getStyleClass().add("node-row");
|
cell.getStyleClass().add("node-row");
|
||||||
} else if(entry instanceof UtxoEntry) {
|
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||||
cell.getStyleClass().add("utxo-row");
|
cell.getStyleClass().add("utxo-row");
|
||||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
|
||||||
if(!utxoEntry.isSpendable()) {
|
if(!utxoEntry.isSpendable()) {
|
||||||
cell.getStyleClass().add("unspendable");
|
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");
|
cell.getStyleClass().add("hashindex-row");
|
||||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
|
||||||
if(hashIndexEntry.isSpent()) {
|
if(hashIndexEntry.isSpent()) {
|
||||||
cell.getStyleClass().add("spent");
|
cell.getStyleClass().add("spent");
|
||||||
}
|
}
|
||||||
|
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||||
|
cell.getStyleClass().add("unconfirmed-row");
|
||||||
|
} 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")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
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;
|
||||||
|
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, 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<>() {
|
||||||
|
@Override
|
||||||
|
public String toString(Double object) {
|
||||||
|
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
|
||||||
|
public Double fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
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() {
|
||||||
|
addFeeRangeTrackHighlight(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFeeRangeTrackHighlight(int count) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Node track = lookup(".track");
|
||||||
|
if(track != null) {
|
||||||
|
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||||
|
String highlight = "";
|
||||||
|
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
|
||||||
|
highlight += "#a0a1a766 " + getPercentageOfFeeRange(targetBlocksFeeRates.get(Integer.MAX_VALUE)) + "%, ";
|
||||||
|
}
|
||||||
|
highlight += "#41a9c966 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_TWO_HOURS - 1) + "%, ";
|
||||||
|
highlight += "#fba71b66 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HOUR - 1) + "%, ";
|
||||||
|
highlight += "#c8416466 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HALF_HOUR - 1) + "%";
|
||||||
|
|
||||||
|
track.setStyle("-fx-background-color: " +
|
||||||
|
"-fx-shadow-highlight-color, " +
|
||||||
|
"linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), " +
|
||||||
|
"linear-gradient(to bottom, derive(-fx-control-inner-background, -9%), derive(-fx-control-inner-background, 0%), derive(-fx-control-inner-background, -5%), derive(-fx-control-inner-background, -12%)), " +
|
||||||
|
"linear-gradient(to right, " + highlight + ")");
|
||||||
|
} else if(count < 20) {
|
||||||
|
addFeeRangeTrackHighlight(count+1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -> getFallbackFeeRate(),
|
||||||
|
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
|
||||||
|
LinkedHashMap::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
return retrievedFeeRates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPercentageOfFeeRange(Map<Integer, Double> targetBlocksFeeRates, Integer minTargetBlocks) {
|
||||||
|
List<Integer> rates = new ArrayList<>(targetBlocksFeeRates.keySet());
|
||||||
|
Collections.reverse(rates);
|
||||||
|
for(Integer targetBlocks : rates) {
|
||||||
|
if(targetBlocks < minTargetBlocks) {
|
||||||
|
return getPercentageOfFeeRange(targetBlocksFeeRates.get(targetBlocks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPercentageOfFeeRange(Double feeRate) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
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.wallet.Entry;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.control.TreeTableCell;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Currency;
|
||||||
|
|
||||||
|
public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||||
|
private final Tooltip tooltip;
|
||||||
|
private final FiatContextMenu contextMenu;
|
||||||
|
|
||||||
|
public FiatCell() {
|
||||||
|
super();
|
||||||
|
tooltip = new Tooltip();
|
||||||
|
contextMenu = new FiatContextMenu();
|
||||||
|
getStyleClass().add("coin-cell");
|
||||||
|
if(OsType.getCurrent() == OsType.MACOS) {
|
||||||
|
getStyleClass().add("number-field");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Number amount, boolean empty) {
|
||||||
|
super.updateItem(amount, empty);
|
||||||
|
|
||||||
|
if(empty || amount == null) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
|
setContextMenu(null);
|
||||||
|
} else {
|
||||||
|
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
|
EntryCell.applyRowStyles(this, entry);
|
||||||
|
|
||||||
|
CoinTreeTable coinTreeTable = (CoinTreeTable) getTreeTableView();
|
||||||
|
UnitFormat format = coinTreeTable.getUnitFormat();
|
||||||
|
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
|
||||||
|
|
||||||
|
if(currencyRate != null && currencyRate.isAvailable()) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
|
||||||
|
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
|
||||||
|
|
||||||
|
setText(label);
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(tooltip);
|
||||||
|
setContextMenu(contextMenu);
|
||||||
|
} else {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
|
setContextMenu(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FiatContextMenu extends ContextMenu {
|
||||||
|
public FiatContextMenu() {
|
||||||
|
MenuItem copyValue = new MenuItem("Copy Value");
|
||||||
|
copyValue.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(getText());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
MenuItem copyRate = new MenuItem("Copy Rate");
|
||||||
|
copyRate.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(getTooltip().getText());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().addAll(copyValue, copyRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
|
|
@ -10,14 +12,9 @@ import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.text.DecimalFormatSymbols;
|
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class FiatLabel extends CopyableLabel {
|
public class FiatLabel extends CopyableLabel {
|
||||||
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
|
||||||
|
|
||||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||||
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
|
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
|
||||||
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
@ -30,9 +27,9 @@ public class FiatLabel extends CopyableLabel {
|
||||||
|
|
||||||
public FiatLabel(String text) {
|
public FiatLabel(String text) {
|
||||||
super(text);
|
super(text);
|
||||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
|
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat()));
|
||||||
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||||
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||||
tooltip = new Tooltip();
|
tooltip = new Tooltip();
|
||||||
contextMenu = new FiatContextMenu();
|
contextMenu = new FiatContextMenu();
|
||||||
}
|
}
|
||||||
|
|
@ -83,14 +80,22 @@ public class FiatLabel extends CopyableLabel {
|
||||||
setCurrency(currency);
|
setCurrency(currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setValueAsText(long balance) {
|
public void refresh() {
|
||||||
|
refresh(Config.get().getUnitFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh(UnitFormat unitFormat) {
|
||||||
|
setValueAsText(getValue(), unitFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setValueAsText(long balance, UnitFormat unitFormat) {
|
||||||
if(getCurrency() != null && getBtcRate() > 0.0) {
|
if(getCurrency() != null && getBtcRate() > 0.0) {
|
||||||
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
||||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
||||||
|
|
||||||
String label = getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(fiatBalance.doubleValue());
|
String label = getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(fiatBalance.doubleValue());
|
||||||
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(getBtcRate()));
|
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(getBtcRate()));
|
||||||
|
|
||||||
setText(label);
|
setText(label);
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.sparrowwallet.drongo.OsType;
|
||||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.FileImport;
|
import com.sparrowwallet.sparrow.io.FileImport;
|
||||||
|
|
@ -24,9 +26,7 @@ import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.controlsfx.control.SegmentedButton;
|
import org.controlsfx.control.SegmentedButton;
|
||||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.controlsfx.tools.Platform;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
protected ButtonBase importButton;
|
protected ButtonBase importButton;
|
||||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||||
private final boolean scannable;
|
private final boolean scannable;
|
||||||
|
private final boolean fileFormatAvailable;
|
||||||
protected List<Wallet> wallets;
|
protected List<Wallet> wallets;
|
||||||
|
|
||||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
|
||||||
super(title, description, content, imageUrl);
|
super(title, description, content, walletModel);
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.scannable = scannable;
|
this.scannable = scannable;
|
||||||
|
this.fileFormatAvailable = fileFormatAvailable;
|
||||||
|
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
buttonBox.getChildren().add(createButton());
|
buttonBox.getChildren().add(createButton());
|
||||||
|
|
@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Control createButton() {
|
protected Control createButton() {
|
||||||
if(scannable) {
|
if(scannable && fileFormatAvailable) {
|
||||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
cameraGlyph.setFontSize(12);
|
cameraGlyph.setFontSize(12);
|
||||||
|
|
@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
SegmentedButton segmentedButton = new SegmentedButton();
|
SegmentedButton segmentedButton = new SegmentedButton();
|
||||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||||
return segmentedButton;
|
return segmentedButton;
|
||||||
|
} else if(scannable) {
|
||||||
|
importButton = new Button("Scan...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
importButton.setGraphic(cameraGlyph);
|
||||||
|
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
importButton.setOnAction(event -> {
|
||||||
|
importQR();
|
||||||
|
});
|
||||||
|
return importButton;
|
||||||
} else {
|
} else {
|
||||||
importButton = new Button("Import File...");
|
importButton = new Button("Import File...");
|
||||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
@ -92,7 +104,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
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("JSON", "*.json"),
|
||||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||||
);
|
);
|
||||||
|
|
@ -138,6 +150,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
|
|
||||||
private void importQR() {
|
private void importQR() {
|
||||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
qrScanDialog.initOwner(this.getScene().getWindow());
|
||||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||||
if(optionalResult.isPresent()) {
|
if(optionalResult.isPresent()) {
|
||||||
QRScanDialog.Result result = optionalResult.get();
|
QRScanDialog.Result result = optionalResult.get();
|
||||||
|
|
@ -146,6 +159,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
try {
|
try {
|
||||||
importFile(importer.getName(), null, null);
|
importFile(importer.getName(), null, null);
|
||||||
} catch(ImportException e) {
|
} catch(ImportException e) {
|
||||||
|
log.error("Error importing QR", e);
|
||||||
|
setError("Import Error", e.getMessage());
|
||||||
|
}
|
||||||
|
} else if(result.outputDescriptor != null) {
|
||||||
|
try {
|
||||||
|
wallets = List.of(result.outputDescriptor.toWallet());
|
||||||
|
importFile(importer.getName(), null, null);
|
||||||
|
} catch(ImportException e) {
|
||||||
|
log.error("Error importing QR", e);
|
||||||
setError("Import Error", e.getMessage());
|
setError("Import Error", e.getMessage());
|
||||||
}
|
}
|
||||||
} else if(result.payload != null) {
|
} else if(result.payload != null) {
|
||||||
|
|
@ -165,10 +187,17 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
} else if(result.exception != null) {
|
} else if(result.exception != null) {
|
||||||
log.error("Error importing QR", result.exception);
|
log.error("Error importing QR", result.exception);
|
||||||
setError("Import Error", result.exception.getMessage());
|
setError("Import Error", result.exception.getMessage());
|
||||||
|
} else {
|
||||||
|
setError("Import Error", null);
|
||||||
|
setExpanded(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<Wallet> getScannedWallets() {
|
||||||
|
return wallets;
|
||||||
|
}
|
||||||
|
|
||||||
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
|
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
|
||||||
if(wallets != null) {
|
if(wallets != null) {
|
||||||
for(Wallet wallet : wallets) {
|
for(Wallet wallet : wallets) {
|
||||||
|
|
@ -190,8 +219,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
||||||
|
|
||||||
private Node getPasswordEntry(File file) {
|
private Node getPasswordEntry(File file) {
|
||||||
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
|
CustomPasswordField passwordField = new ViewPasswordField();
|
||||||
passwordField.setPromptText("Wallet password");
|
passwordField.setPromptText("Password");
|
||||||
password.bind(passwordField.textProperty());
|
password.bind(passwordField.textProperty());
|
||||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
|
@ -211,6 +240,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
contentBox.setPrefHeight(60);
|
contentBox.setPrefHeight(60);
|
||||||
|
|
||||||
|
javafx.application.Platform.runLater(passwordField::requestFocus);
|
||||||
|
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.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;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.controlsfx.control.SegmentedButton;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||||
|
private final Keystore keystore;
|
||||||
|
private final KeystoreFileExport exporter;
|
||||||
|
private final boolean scannable;
|
||||||
|
private final boolean file;
|
||||||
|
|
||||||
|
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||||
|
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
|
||||||
|
this.keystore = keystore;
|
||||||
|
this.exporter = exporter;
|
||||||
|
this.scannable = exporter.isKeystoreExportScannable();
|
||||||
|
this.file = exporter.isKeystoreExportFile();
|
||||||
|
|
||||||
|
buttonBox.getChildren().clear();
|
||||||
|
buttonBox.getChildren().add(createButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Control createButton() {
|
||||||
|
if(scannable && file) {
|
||||||
|
ToggleButton showButton = new ToggleButton("Show...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
showButton.setGraphic(cameraGlyph);
|
||||||
|
showButton.setOnAction(event -> {
|
||||||
|
showButton.setSelected(false);
|
||||||
|
exportQR();
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleButton fileButton = new ToggleButton("Export File...");
|
||||||
|
fileButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
fileButton.setOnAction(event -> {
|
||||||
|
fileButton.setSelected(false);
|
||||||
|
exportFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
SegmentedButton segmentedButton = new SegmentedButton();
|
||||||
|
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||||
|
return segmentedButton;
|
||||||
|
} else if(scannable) {
|
||||||
|
Button showButton = new Button("Show...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
showButton.setGraphic(cameraGlyph);
|
||||||
|
showButton.setOnAction(event -> {
|
||||||
|
exportQR();
|
||||||
|
});
|
||||||
|
return showButton;
|
||||||
|
} else {
|
||||||
|
Button exportButton = new Button("Export File...");
|
||||||
|
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
exportButton.setOnAction(event -> {
|
||||||
|
exportFile();
|
||||||
|
});
|
||||||
|
return exportButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportQR() {
|
||||||
|
exportKeystore(null, keystore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportFile() {
|
||||||
|
Stage window = new Stage();
|
||||||
|
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||||
|
String extension = exporter.getExportFileExtension(keystore);
|
||||||
|
String fileName = keystore.getLabel();
|
||||||
|
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||||
|
|
||||||
|
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||||
|
File file = fileChooser.showSaveDialog(window);
|
||||||
|
if(file != null) {
|
||||||
|
exportKeystore(file, keystore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportKeystore(File file, Keystore exportKeystore) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
exporter.exportKeystore(exportKeystore, baos);
|
||||||
|
|
||||||
|
if(exporter.requiresSignature()) {
|
||||||
|
String message = baos.toString(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
|
||||||
|
TextAreaDialog dialog = new TextAreaDialog(message, false);
|
||||||
|
dialog.initOwner(this.getScene().getWindow());
|
||||||
|
dialog.setTitle("Sign " + exporter.getName() + " Export");
|
||||||
|
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
|
||||||
|
dialog.showAndWait();
|
||||||
|
|
||||||
|
Wallet wallet = new Wallet();
|
||||||
|
wallet.setScriptType(ScriptType.P2PKH);
|
||||||
|
wallet.getKeystores().add(keystore);
|
||||||
|
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
|
||||||
|
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
|
||||||
|
deviceSignMessageDialog.initOwner(this.getScene().getWindow());
|
||||||
|
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||||
|
if(optSignature.isPresent()) {
|
||||||
|
exporter.addSignature(keystore, optSignature.get(), baos);
|
||||||
|
}
|
||||||
|
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||||
|
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
|
||||||
|
exporter.addSignature(keystore, signature, baos);
|
||||||
|
} else {
|
||||||
|
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
|
||||||
|
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
|
||||||
|
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
|
||||||
|
throw new RuntimeException("Export aborted due to lack of device message signing support.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(file != null) {
|
||||||
|
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
outputStream.write(baos.toByteArray());
|
||||||
|
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QRDisplayDialog qrDisplayDialog;
|
||||||
|
if(exporter instanceof Bip129) {
|
||||||
|
UR ur = UR.fromBytes(baos.toByteArray());
|
||||||
|
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
|
||||||
|
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||||
|
} else {
|
||||||
|
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
qrDisplayDialog.initOwner(buttonBox.getScene().getWindow());
|
||||||
|
qrDisplayDialog.showAndWait();
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
String errorMessage = e.getMessage();
|
||||||
|
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||||
|
errorMessage = e.getCause().getMessage();
|
||||||
|
}
|
||||||
|
setError("Export Error", errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
private final KeyDerivation requiredDerivation;
|
private final KeyDerivation requiredDerivation;
|
||||||
|
|
||||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
this.requiredDerivation = requiredDerivation;
|
this.requiredDerivation = requiredDerivation;
|
||||||
|
|
@ -29,7 +29,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
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 {
|
} else {
|
||||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
|
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
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.event.WalletExportEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
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.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.Control;
|
||||||
|
|
@ -21,18 +29,23 @@ import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
|
||||||
|
|
||||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private final WalletExport exporter;
|
private final WalletExport exporter;
|
||||||
private final boolean scannable;
|
private final boolean scannable;
|
||||||
|
private final boolean file;
|
||||||
|
|
||||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||||
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.exporter = exporter;
|
this.exporter = exporter;
|
||||||
this.scannable = exporter.isWalletExportScannable();
|
this.scannable = exporter.isWalletExportScannable();
|
||||||
|
this.file = exporter.isWalletExportFile();
|
||||||
|
|
||||||
buttonBox.getChildren().clear();
|
buttonBox.getChildren().clear();
|
||||||
buttonBox.getChildren().add(createButton());
|
buttonBox.getChildren().add(createButton());
|
||||||
|
|
@ -40,7 +53,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Control createButton() {
|
protected Control createButton() {
|
||||||
if(scannable) {
|
if(scannable && file) {
|
||||||
ToggleButton showButton = new ToggleButton("Show...");
|
ToggleButton showButton = new ToggleButton("Show...");
|
||||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
cameraGlyph.setFontSize(12);
|
cameraGlyph.setFontSize(12);
|
||||||
|
|
@ -60,6 +73,15 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
SegmentedButton segmentedButton = new SegmentedButton();
|
SegmentedButton segmentedButton = new SegmentedButton();
|
||||||
segmentedButton.getButtons().addAll(showButton, fileButton);
|
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||||
return segmentedButton;
|
return segmentedButton;
|
||||||
|
} else if(scannable) {
|
||||||
|
Button showButton = new Button("Show...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
showButton.setGraphic(cameraGlyph);
|
||||||
|
showButton.setOnAction(event -> {
|
||||||
|
exportQR();
|
||||||
|
});
|
||||||
|
return showButton;
|
||||||
} else {
|
} else {
|
||||||
Button exportButton = new Button("Export File...");
|
Button exportButton = new Button("Export File...");
|
||||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
@ -80,9 +102,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||||
String extension = exporter.getExportFileExtension(wallet);
|
String extension = exporter.getExportFileExtension(wallet);
|
||||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
|
String walletModel = exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||||
if(exporter instanceof Sparrow) {
|
String postfix = walletModel.equals(extension) ? "" : "-" + walletModel;
|
||||||
fileName = wallet.getMasterName();
|
String fileName = wallet.getFullName() + postfix;
|
||||||
|
if(exporter.exportsAllWallets()) {
|
||||||
|
fileName = wallet.getMasterName() + postfix;
|
||||||
}
|
}
|
||||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||||
|
|
||||||
|
|
@ -97,19 +121,16 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
|
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
|
||||||
Wallet copy = wallet.copy();
|
Wallet copy = wallet.copy();
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
dlg.initOwner(buttonBox.getScene().getWindow());
|
||||||
Optional<SecureString> password = dlg.showAndWait();
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
if(password.isPresent()) {
|
if(password.isPresent()) {
|
||||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||||
|
String walletPassword = password.get().asString();
|
||||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
|
exportWallet(file, decryptedWallet, walletPassword);
|
||||||
try {
|
|
||||||
exportWallet(file, decryptedWallet);
|
|
||||||
} finally {
|
|
||||||
decryptedWallet.clearPrivate();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||||
|
|
@ -119,28 +140,53 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
decryptWalletService.start();
|
decryptWalletService.start();
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
try {
|
||||||
if(file != null) {
|
if(file != null) {
|
||||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
|
||||||
exporter.exportWallet(exportWallet, outputStream);
|
fileWalletExportService.setOnSucceeded(event -> {
|
||||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
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 {
|
} else {
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
exporter.exportWallet(exportWallet, outputStream);
|
exporter.exportWallet(exportWallet, outputStream, password);
|
||||||
QRDisplayDialog qrDisplayDialog;
|
QRDisplayDialog qrDisplayDialog;
|
||||||
if(exporter instanceof CoboVaultMultisig) {
|
if(exporter instanceof CoboVaultMultisig) {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
|
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
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, false);
|
||||||
|
} else if(exporter instanceof Descriptor) {
|
||||||
|
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||||
|
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||||
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||||
|
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||||
|
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||||
|
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
||||||
|
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||||
|
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||||
|
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||||
|
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||||
} else {
|
} else {
|
||||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
qrDisplayDialog.initOwner(buttonBox.getScene().getWindow());
|
||||||
qrDisplayDialog.showAndWait();
|
qrDisplayDialog.showAndWait();
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
@ -149,6 +195,42 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
errorMessage = e.getCause().getMessage();
|
errorMessage = e.getCause().getMessage();
|
||||||
}
|
}
|
||||||
setError("Export Error", errorMessage);
|
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,13 +12,19 @@ public class FileWalletImportPane extends FileImportPane {
|
||||||
private final WalletImport importer;
|
private final WalletImport importer;
|
||||||
|
|
||||||
public FileWalletImportPane(WalletImport importer) {
|
public FileWalletImportPane(WalletImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||||
Wallet wallet = importer.importWallet(inputStream, password);
|
Wallet wallet;
|
||||||
|
if(getScannedWallets() != null && !getScannedWallets().isEmpty()) {
|
||||||
|
wallet = getScannedWallets().iterator().next();
|
||||||
|
} else {
|
||||||
|
wallet = importer.importWallet(inputStream, password);
|
||||||
|
}
|
||||||
|
|
||||||
if(wallet.getName() == null) {
|
if(wallet.getName() == null) {
|
||||||
wallet.setName(fileName);
|
wallet.setName(fileName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||||
import com.sparrowwallet.sparrow.io.ImportException;
|
import com.sparrowwallet.sparrow.io.ImportException;
|
||||||
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
|
@ -22,6 +23,7 @@ import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -37,14 +39,16 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
private final KeystoreFileImport importer;
|
private final KeystoreFileImport importer;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private byte[] fileBytes;
|
private byte[] fileBytes;
|
||||||
|
private String password;
|
||||||
|
|
||||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||||
this.importer = importer;
|
this.importer = importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
|
this.password = password;
|
||||||
|
|
||||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||||
if(wallets != null && !wallets.isEmpty()) {
|
if(wallets != null && !wallets.isEmpty()) {
|
||||||
|
|
@ -82,7 +86,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
EventManager.get().post(new WalletImportEvent(wallet));
|
EventManager.get().post(new WalletImportEvent(wallet));
|
||||||
} else {
|
} else {
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||||
|
|
||||||
Wallet wallet = new Wallet();
|
Wallet wallet = new Wallet();
|
||||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||||
|
|
@ -97,13 +101,29 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
|
|
||||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||||
Label label = new Label("Script Type:");
|
Label label = new Label("Script Type:");
|
||||||
|
|
||||||
|
HBox fieldBox = new HBox(5);
|
||||||
|
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||||
}
|
}
|
||||||
|
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||||
|
@Override
|
||||||
|
public String toString(ScriptType scriptType) {
|
||||||
|
return scriptType == null ? "" : scriptType.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptType fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
scriptTypeComboBox.setMaxWidth(170);
|
||||||
|
|
||||||
HelpLabel helpLabel = new HelpLabel();
|
HelpLabel helpLabel = new HelpLabel();
|
||||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||||
|
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||||
|
|
||||||
Region region = new Region();
|
Region region = new Region();
|
||||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||||
|
|
@ -130,10 +150,12 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||||
HBox contentBox = new HBox();
|
HBox contentBox = new HBox();
|
||||||
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
contentBox.setSpacing(20);
|
contentBox.setSpacing(20);
|
||||||
contentBox.getChildren().addAll(label, scriptTypeComboBox, helpLabel, region, importFileButton);
|
contentBox.getChildren().addAll(label, fieldBox, region, importFileButton);
|
||||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
contentBox.setPrefHeight(60);
|
contentBox.setPrefHeight(60);
|
||||||
|
|
||||||
|
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||||
|
|
||||||
return contentBox;
|
return contentBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public class HelpLabel extends Label {
|
||||||
tooltip.textProperty().bind(helpTextProperty());
|
tooltip.textProperty().bind(helpTextProperty());
|
||||||
tooltip.graphicProperty().bind(helpGraphicProperty());
|
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||||
tooltip.setShowDuration(Duration.seconds(15));
|
tooltip.setShowDuration(Duration.seconds(15));
|
||||||
|
tooltip.setShowDelay(Duration.millis(500));
|
||||||
getStyleClass().add("help-label");
|
getStyleClass().add("help-label");
|
||||||
|
|
||||||
Platform.runLater(() -> setTooltip(tooltip));
|
Platform.runLater(() -> setTooltip(tooltip));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import javafx.beans.NamedArg;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.SpinnerValueFactory;
|
||||||
|
import javafx.util.converter.IntegerStringConverter;
|
||||||
|
|
||||||
|
public class IntegerSpinner extends Spinner<Integer> {
|
||||||
|
public IntegerSpinner() {
|
||||||
|
super();
|
||||||
|
setupEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerSpinner(@NamedArg("min") int min,
|
||||||
|
@NamedArg("max") int max,
|
||||||
|
@NamedArg("initialValue") int initialValue) {
|
||||||
|
super(min, max, initialValue);
|
||||||
|
setupEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerSpinner(@NamedArg("min") int min,
|
||||||
|
@NamedArg("max") int max,
|
||||||
|
@NamedArg("initialValue") int initialValue,
|
||||||
|
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||||
|
super(min, max, initialValue, amountToStepBy);
|
||||||
|
setupEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupEditor() {
|
||||||
|
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue != null && !newValue) {
|
||||||
|
commitValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(!newValue.matches("\\d*")) {
|
||||||
|
getEditor().setText(newValue.replaceAll("[^\\d]", ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ValueFactory extends SpinnerValueFactory.IntegerSpinnerValueFactory {
|
||||||
|
public ValueFactory(@NamedArg("min") int min,
|
||||||
|
@NamedArg("max") int max) {
|
||||||
|
super(min, max);
|
||||||
|
setupConverter(min);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueFactory(@NamedArg("min") int min,
|
||||||
|
@NamedArg("max") int max,
|
||||||
|
@NamedArg("initialValue") int initialValue) {
|
||||||
|
super(min, max, initialValue);
|
||||||
|
setupConverter(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueFactory(@NamedArg("min") int min,
|
||||||
|
@NamedArg("max") int max,
|
||||||
|
@NamedArg("initialValue") int initialValue,
|
||||||
|
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||||
|
super(min, max, initialValue, amountToStepBy);
|
||||||
|
setupConverter(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupConverter(Integer defaultValue) {
|
||||||
|
setConverter(new IntegerStringConverter() {
|
||||||
|
@Override
|
||||||
|
public Integer fromString(String value) {
|
||||||
|
if(value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
if(value.length() < 1) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.valueOf(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeystoreExportDialog extends Dialog<Keystore> {
|
||||||
|
public KeystoreExportDialog(Keystore keystore) {
|
||||||
|
EventManager.get().register(this);
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
|
AnchorPane anchorPane = new AnchorPane();
|
||||||
|
stackPane.getChildren().add(anchorPane);
|
||||||
|
|
||||||
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
|
scrollPane.setPrefHeight(200);
|
||||||
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
anchorPane.getChildren().add(scrollPane);
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||||
|
|
||||||
|
List<KeystoreFileExport> exporters = List.of(new Bip129());
|
||||||
|
|
||||||
|
Accordion exportAccordion = new Accordion();
|
||||||
|
for(KeystoreFileExport exporter : exporters) {
|
||||||
|
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||||
|
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
|
||||||
|
exportAccordion.getPanes().add(exportPane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||||
|
scrollPane.setContent(exportAccordion);
|
||||||
|
|
||||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
dialogPane.setPrefWidth(500);
|
||||||
|
dialogPane.setPrefHeight(280);
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void keystoreExported(KeystoreExportEvent event) {
|
||||||
|
setResult(event.getKeystore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.scene.control.Dialog;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
public class KeystorePassphraseDialog extends Dialog<String> {
|
public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
private final CustomPasswordField passphrase;
|
private final CustomPasswordField passphrase;
|
||||||
|
private final ObjectProperty<byte[]> masterFingerprint = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
public KeystorePassphraseDialog(Keystore keystore) {
|
public KeystorePassphraseDialog(Keystore keystore) {
|
||||||
this(null, keystore);
|
this(null, keystore);
|
||||||
|
|
@ -25,7 +28,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
|
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
|
||||||
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
|
this.passphrase = new ViewPasswordField();
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
|
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
|
||||||
|
|
@ -45,10 +48,38 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
content.setPrefHeight(50);
|
content.setPrefHeight(50);
|
||||||
content.getChildren().add(passphrase);
|
content.getChildren().add(passphrase);
|
||||||
|
|
||||||
|
passphrase.textProperty().addListener((observable, oldValue, passphrase) -> {
|
||||||
|
masterFingerprint.set(getMasterFingerprint(keystore, passphrase));
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox fingerprintBox = new HBox(10);
|
||||||
|
fingerprintBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
Label fingerprintLabel = new Label("Master fingerprint:");
|
||||||
|
TextField fingerprintHex = new TextField();
|
||||||
|
fingerprintHex.setDisable(true);
|
||||||
|
fingerprintHex.setMaxWidth(80);
|
||||||
|
fingerprintHex.getStyleClass().addAll("fixed-width");
|
||||||
|
fingerprintHex.setStyle("-fx-opacity: 0.6");
|
||||||
|
masterFingerprint.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue != null) {
|
||||||
|
fingerprintHex.setText(Utils.bytesToHex(newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LifeHashIcon lifeHashIcon = new LifeHashIcon();
|
||||||
|
lifeHashIcon.dataProperty().bind(masterFingerprint);
|
||||||
|
HelpLabel helpLabel = new HelpLabel();
|
||||||
|
helpLabel.setHelpText("All passphrases create valid wallets." +
|
||||||
|
"\nThe master fingerprint identifies the keystore and changes as the passphrase changes." +
|
||||||
|
"\n" + (confirm ? "Take a moment to identify it before proceeding." : "Make sure you recognise it before proceeding."));
|
||||||
|
fingerprintBox.getChildren().addAll(fingerprintLabel, fingerprintHex, lifeHashIcon, helpLabel);
|
||||||
|
content.getChildren().add(fingerprintBox);
|
||||||
|
|
||||||
|
masterFingerprint.set(getMasterFingerprint(keystore, ""));
|
||||||
|
|
||||||
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||||
warnGlyph.getStyleClass().add("warn-icon");
|
warnGlyph.getStyleClass().add("warn-icon");
|
||||||
warnGlyph.setFontSize(12);
|
warnGlyph.setFontSize(12);
|
||||||
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
|
Label warnLabel = new Label((confirm ? "Note" : "Check") + " the master fingerprint before proceeding!", warnGlyph);
|
||||||
warnLabel.setGraphicTextGap(5);
|
warnLabel.setGraphicTextGap(5);
|
||||||
content.getChildren().add(warnLabel);
|
content.getChildren().add(warnLabel);
|
||||||
|
|
||||||
|
|
@ -57,4 +88,14 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
|
|
||||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
|
||||||
|
try {
|
||||||
|
Keystore copyKeystore = keystore.copy();
|
||||||
|
copyKeystore.getSeed().setPassphrase(passphrase);
|
||||||
|
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Persistable;
|
||||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
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.event.Event;
|
||||||
|
import javafx.geometry.Point2D;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||||
import javafx.scene.input.Clipboard;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.input.ClipboardContent;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.input.DataFormat;
|
import javafx.scene.input.DataFormat;
|
||||||
|
import javafx.util.Duration;
|
||||||
import javafx.util.converter.DefaultStringConverter;
|
import javafx.util.converter.DefaultStringConverter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
class LabelCell extends TextFieldTreeTableCell<Entry, String> implements ConfirmationsListener {
|
||||||
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
|
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
|
||||||
|
|
||||||
|
private IntegerProperty confirmationsProperty;
|
||||||
|
|
||||||
public LabelCell() {
|
public LabelCell() {
|
||||||
super(new DefaultStringConverter());
|
super(new DefaultStringConverter());
|
||||||
getStyleClass().add("label-cell");
|
getStyleClass().add("label-cell");
|
||||||
|
|
@ -28,12 +38,23 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
if(empty) {
|
if(empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
setTooltip(null);
|
||||||
} else {
|
} else {
|
||||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||||
EntryCell.applyRowStyles(this, entry);
|
EntryCell.applyRowStyles(this, entry);
|
||||||
|
|
||||||
setText(label);
|
setText(label);
|
||||||
setContextMenu(new LabelContextMenu(entry, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +62,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
public void commitEdit(String label) {
|
public void commitEdit(String label) {
|
||||||
if(label != null) {
|
if(label != null) {
|
||||||
label = label.trim();
|
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
|
// This block is necessary to support commit on losing focus, because
|
||||||
|
|
@ -60,6 +95,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
super.commitEdit(label);
|
super.commitEdit(label);
|
||||||
|
Platform.runLater(() -> getTreeTableView().requestFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -81,7 +117,22 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LabelContextMenu extends ContextMenu {
|
@Override
|
||||||
|
public IntegerProperty getConfirmationsProperty() {
|
||||||
|
if(confirmationsProperty == null) {
|
||||||
|
confirmationsProperty = new SimpleIntegerProperty();
|
||||||
|
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||||
|
getStyleClass().remove("confirming");
|
||||||
|
confirmationsProperty.unbind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmationsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LabelContextMenu extends ContextMenu {
|
||||||
public LabelContextMenu(Entry entry, String label) {
|
public LabelContextMenu(Entry entry, String label) {
|
||||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||||
copyLabel.setOnAction(AE -> {
|
copyLabel.setOnAction(AE -> {
|
||||||
|
|
@ -92,18 +143,22 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||||
});
|
});
|
||||||
getItems().add(copyLabel);
|
getItems().add(copyLabel);
|
||||||
|
|
||||||
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||||
if(content instanceof String) {
|
pasteLabel.setOnAction(AE -> {
|
||||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
hide();
|
||||||
pasteLabel.setOnAction(AE -> {
|
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||||
hide();
|
if(currentContent instanceof String) {
|
||||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
entry.labelProperty().set((String)currentContent);
|
||||||
if(currentContent instanceof String) {
|
}
|
||||||
entry.labelProperty().set((String)currentContent);
|
});
|
||||||
}
|
getItems().add(pasteLabel);
|
||||||
});
|
|
||||||
getItems().add(pasteLabel);
|
MenuItem editLabel = new MenuItem("Edit Label...");
|
||||||
}
|
editLabel.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
startEdit();
|
||||||
|
});
|
||||||
|
getItems().add(editLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||||
|
import com.sparrowwallet.toucan.LifeHash;
|
||||||
|
import com.sparrowwallet.toucan.LifeHashVersion;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.scene.Group;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.paint.ImagePattern;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class LifeHashIcon extends Group {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LifeHashIcon.class);
|
||||||
|
|
||||||
|
private static final int SIZE = 24;
|
||||||
|
|
||||||
|
private final ObjectProperty<byte[]> dataProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
public LifeHashIcon() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
dataProperty.addListener((observable, oldValue, data) -> {
|
||||||
|
if(data == null) {
|
||||||
|
getChildren().clear();
|
||||||
|
} else if(oldValue == null || !Arrays.equals(oldValue, data)) {
|
||||||
|
LifeHash.Image lifeHashImage = LifeHash.makeFromData(data, LifeHashVersion.VERSION2, 1, false);
|
||||||
|
BufferedImage bufferedImage = LifeHash.getBufferedImage(lifeHashImage);
|
||||||
|
BufferedImage resizedImage = ImageUtils.resizeToImage(bufferedImage, SIZE, SIZE);
|
||||||
|
Image image = SwingFXUtils.toFXImage(resizedImage, null);
|
||||||
|
setImage(image);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setImage(Image image) {
|
||||||
|
getChildren().clear();
|
||||||
|
Rectangle rectangle = new Rectangle(SIZE, SIZE);
|
||||||
|
rectangle.setArcWidth(6);
|
||||||
|
rectangle.setArcHeight(6);
|
||||||
|
rectangle.setFill(new ImagePattern(image));
|
||||||
|
rectangle.setStroke(Color.rgb(65, 72, 77));
|
||||||
|
rectangle.setStrokeWidth(1.0);
|
||||||
|
getChildren().add(rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return dataProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<byte[]> dataProperty() {
|
||||||
|
return dataProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(byte[] data) {
|
||||||
|
this.dataProperty.set(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHex(String hex) {
|
||||||
|
setData(hex == null ? null : Utils.hexToBytes(hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
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;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.chart.*;
|
import javafx.scene.chart.*;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import org.controlsfx.control.SegmentedButton;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
|
@ -27,16 +39,100 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||||
public static final int MAX_PERIOD_HOURS = 2;
|
public static final int DEFAULT_MAX_PERIOD_HOURS = 2;
|
||||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||||
|
private static final List<Integer> 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);
|
||||||
|
|
||||||
|
private int maxPeriodHours = DEFAULT_MAX_PERIOD_HOURS;
|
||||||
private Tooltip tooltip;
|
private Tooltip tooltip;
|
||||||
|
|
||||||
|
private MempoolSizeFeeRatesChart expandedChart;
|
||||||
|
private final EventHandler<MouseEvent> expandedChartHandler = new EventHandler<>() {
|
||||||
|
@Override
|
||||||
|
public void handle(MouseEvent event) {
|
||||||
|
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||||
|
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||||
|
stage.setTitle("Mempool by vBytes");
|
||||||
|
stage.initOwner(MempoolSizeFeeRatesChart.this.getScene().getWindow());
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.setResizable(false);
|
||||||
|
|
||||||
|
StackPane scenePane = new StackPane();
|
||||||
|
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||||
|
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||||
|
}
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||||
|
|
||||||
|
VBox vBox = new VBox(20);
|
||||||
|
vBox.setPadding(new Insets(20, 20, 20, 20));
|
||||||
|
|
||||||
|
expandedChart = new MempoolSizeFeeRatesChart();
|
||||||
|
expandedChart.initialize();
|
||||||
|
expandedChart.getStyleClass().add("vsizeChart");
|
||||||
|
expandedChart.update(AppServices.getMempoolHistogram());
|
||||||
|
expandedChart.setLegendVisible(false);
|
||||||
|
expandedChart.setAnimated(false);
|
||||||
|
expandedChart.setPrefWidth(700);
|
||||||
|
|
||||||
|
HBox buttonBox = new HBox();
|
||||||
|
buttonBox.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
|
ToggleGroup periodGroup = new ToggleGroup();
|
||||||
|
ToggleButton period2 = new ToggleButton("2H");
|
||||||
|
ToggleButton period24 = new ToggleButton("24H");
|
||||||
|
SegmentedButton periodButtons = new SegmentedButton(period2, period24);
|
||||||
|
periodButtons.setToggleGroup(periodGroup);
|
||||||
|
periodGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
expandedChart.maxPeriodHours = (newValue == period2 ? 2 : 24);
|
||||||
|
expandedChart.update(AppServices.getMempoolHistogram());
|
||||||
|
});
|
||||||
|
|
||||||
|
Optional<Date> optEarliest = AppServices.getMempoolHistogram().keySet().stream().findFirst();
|
||||||
|
period24.setDisable(optEarliest.isEmpty() || optEarliest.get().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().isAfter(LocalDateTime.now().minusHours(2)));
|
||||||
|
|
||||||
|
Region region = new Region();
|
||||||
|
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||||
|
|
||||||
|
Button button = new Button("Close");
|
||||||
|
button.setOnAction(e -> {
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
buttonBox.getChildren().addAll(periodButtons, region, button);
|
||||||
|
vBox.getChildren().addAll(expandedChart, buttonBox);
|
||||||
|
scenePane.getChildren().add(vBox);
|
||||||
|
|
||||||
|
Scene scene = new Scene(scenePane);
|
||||||
|
AppServices.onEscapePressed(scene, stage::close);
|
||||||
|
AppServices.setStageIcon(stage);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setOnShowing(e -> {
|
||||||
|
AppServices.moveToActiveWindowScreen(stage, 800, 460);
|
||||||
|
});
|
||||||
|
stage.setOnHidden(e -> {
|
||||||
|
expandedChart = null;
|
||||||
|
});
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public MempoolSizeFeeRatesChart() {
|
||||||
|
super(new CategoryAxis(), new NumberAxis());
|
||||||
|
}
|
||||||
|
|
||||||
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||||
super(xAxis, yAxis);
|
super(xAxis, yAxis);
|
||||||
|
setOnMouseClicked(expandedChartHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
getStyleClass().add("vsizeChart");
|
||||||
setCreateSymbols(false);
|
setCreateSymbols(false);
|
||||||
setCursor(Cursor.CROSSHAIR);
|
setCursor(Cursor.CROSSHAIR);
|
||||||
setVerticalGridLinesVisible(false);
|
setVerticalGridLinesVisible(false);
|
||||||
|
|
@ -78,17 +174,18 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
long previousFeeRate = 0;
|
for(int i = 0; i < FEE_RATES_INTERVALS.size(); i++) {
|
||||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
int feeRate = FEE_RATES_INTERVALS.get(i);
|
||||||
|
int nextFeeRate = (i == FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : FEE_RATES_INTERVALS.get(i+1));
|
||||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||||
series.setName(feeRate + "+ vB");
|
series.setName(feeRate + "-" + (nextFeeRate == Integer.MAX_VALUE ? 900 : nextFeeRate));
|
||||||
long seriesTotalVSize = 0;
|
long seriesTotalVSize = 0;
|
||||||
|
|
||||||
for(Date date : periodRateSizes.keySet()) {
|
for(Date date : periodRateSizes.keySet()) {
|
||||||
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
||||||
long totalVSize = 0;
|
long totalVSize = 0;
|
||||||
for(MempoolRateSize rateSize : rateSizes) {
|
for(MempoolRateSize rateSize : rateSizes) {
|
||||||
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
|
if(rateSize.getFee() >= feeRate && rateSize.getFee() < nextFeeRate) {
|
||||||
totalVSize += rateSize.getVSize();
|
totalVSize += rateSize.getVSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,8 +197,19 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
if(seriesTotalVSize > 0) {
|
if(seriesTotalVSize > 0) {
|
||||||
getData().add(series);
|
getData().add(series);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previousFeeRate = feeRate;
|
for(int i = 0; i < getData().size(); i++) {
|
||||||
|
Series<String, Number> series = getData().get(i);
|
||||||
|
Set<Node> nodes = lookupAll(".series" + i);
|
||||||
|
for(Node node : nodes) {
|
||||||
|
if(node.getStyleClass().contains("chart-series-area-line")) {
|
||||||
|
node.setStyle("-fx-stroke: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.2;");
|
||||||
|
} else {
|
||||||
|
node.setStyle("-fx-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.5;");
|
||||||
|
}
|
||||||
|
node.getStyleClass().remove("default-color" + i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final double maxMvB = getMaxMvB(getData());
|
final double maxMvB = getMaxMvB(getData());
|
||||||
|
|
@ -131,6 +239,10 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
numberAxis.setTickLabelsVisible(false);
|
numberAxis.setTickLabelsVisible(false);
|
||||||
numberAxis.setOpacity(0);
|
numberAxis.setOpacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(expandedChart != null) {
|
||||||
|
expandedChart.update(mempoolRateSizes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||||
|
|
@ -138,7 +250,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
return mempoolRateSizes;
|
return mempoolRateSizes;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime period = LocalDateTime.now().minusHours(MAX_PERIOD_HOURS);
|
LocalDateTime period = LocalDateTime.now().minusHours(maxPeriodHours);
|
||||||
return mempoolRateSizes.entrySet().stream().filter(entry -> {
|
return mempoolRateSizes.entrySet().stream().filter(entry -> {
|
||||||
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||||
return dateTime.isAfter(period);
|
return dateTime.isAfter(period);
|
||||||
|
|
@ -200,11 +312,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
double mvb = kvb / 1000;
|
double mvb = kvb / 1000;
|
||||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||||
Label label = new Label(series.getName() + ": " + amount);
|
Label label = new Label(series.getName() + " sats/vB: " + amount);
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||||
if(i < 8) {
|
circle.setStyle("-fx-text-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.7;");
|
||||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
|
||||||
}
|
|
||||||
label.setGraphic(circle);
|
label.setGraphic(circle);
|
||||||
getChildren().add(label);
|
getChildren().add(label);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue