mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
97f312cb93 | ||
|
|
269fd7f0da | ||
|
|
f0a813d031 | ||
|
|
ceb5d85648 | ||
|
|
6b6b23b51a | ||
|
|
180e76f0f8 | ||
|
|
d3b1c51115 | ||
|
|
72cb696451 | ||
|
|
784fa5e1e8 | ||
|
|
37aa3c9712 | ||
|
|
9520f6d218 | ||
|
|
f4810bb568 | ||
|
|
febd5c33a2 | ||
|
|
4b39316821 | ||
|
|
69544a2dc8 | ||
|
|
2a0412320a | ||
|
|
fffd489bcf | ||
|
|
70a6b259ba | ||
|
|
26c177bd00 | ||
|
|
1497b3d3bb | ||
|
|
230f3c3db7 | ||
|
|
813e0f3ab1 | ||
|
|
237f97852d | ||
|
|
97ef580602 | ||
|
|
2debc07375 | ||
|
|
1b3a35fda7 | ||
|
|
23fd597ca5 | ||
|
|
eb012f638e | ||
|
|
63259a2056 | ||
|
|
bad209ea5b | ||
|
|
776fcb3044 | ||
|
|
63ec856e87 | ||
|
|
ada8ca28e8 | ||
|
|
ebd629db3a | ||
|
|
c18a2f4388 | ||
|
|
6f95dbe309 | ||
|
|
576253e651 | ||
|
|
b9d6cb17d4 | ||
|
|
3b730a1711 | ||
|
|
8c0a1932cf | ||
|
|
35b57f9d69 | ||
|
|
d6ad7f4808 | ||
|
|
3e47a49f49 | ||
|
|
4c817d243d | ||
|
|
22b7b659f3 | ||
|
|
9dd6068e69 | ||
|
|
54baee57e1 | ||
|
|
581aaf288e | ||
|
|
2fa47e640d | ||
|
|
ea03dece72 | ||
|
|
8e0b9a3ea0 | ||
|
|
67179127e3 | ||
|
|
4ebee8a8f3 | ||
|
|
2548e77d90 | ||
|
|
58e3b9dcdd | ||
|
|
429b733140 | ||
|
|
56e3a54ae0 | ||
|
|
a934ffa76c | ||
|
|
86a49e0d9a | ||
|
|
761e9c9b7e | ||
|
|
712241873f | ||
|
|
31f9cca33a | ||
|
|
395e90e2a5 | ||
|
|
b2657cdcfb | ||
|
|
6bbae204a6 | ||
|
|
0b55dd8a1e | ||
|
|
f74287697c | ||
|
|
7a3e1dfa1f | ||
|
|
427a6925ee | ||
|
|
90dea201b9 | ||
|
|
c55b19af0f | ||
|
|
e12f7a634a | ||
|
|
ca1f934138 | ||
|
|
4f9b87b74e | ||
|
|
e1e5df78c6 | ||
|
|
cfd06a8513 | ||
|
|
1c1099217b | ||
|
|
6939d8a06a | ||
|
|
dbebade3ab | ||
|
|
5895837b60 | ||
|
|
0f9e9d9c35 | ||
|
|
59452e64ea | ||
|
|
573d5b2c18 | ||
|
|
2f0dc5bea8 | ||
|
|
27f7d5ad38 | ||
|
|
414ffc2648 | ||
|
|
aaca9ffa16 | ||
|
|
44b9cee825 | ||
|
|
c024c351ac | ||
|
|
ed2914f2aa | ||
|
|
0b4785e01c | ||
|
|
0c50c9cb9c | ||
|
|
916c2b6122 | ||
|
|
308a89c958 | ||
|
|
88ebef97d4 | ||
|
|
a42761981c | ||
|
|
b6f047d382 | ||
|
|
8f63d89be8 | ||
|
|
6e6111b47c | ||
|
|
e8af7c70bd | ||
|
|
2fc551e35b | ||
|
|
adb77771aa | ||
|
|
13e01451b7 | ||
|
|
aa10bcfe1a | ||
|
|
f30c00ba8f | ||
|
|
4577a64ad5 | ||
|
|
7371ca2994 | ||
|
|
050c4fc31e | ||
|
|
772370808c | ||
|
|
b22e891b7d | ||
|
|
57b3214c54 | ||
|
|
615b78b497 | ||
|
|
d1ab1db1c5 | ||
|
|
f5ac6a3b73 | ||
|
|
fec45356a2 | ||
|
|
37c4ff4dd7 | ||
|
|
b0877d94bf | ||
|
|
a3e4342d7d | ||
|
|
90355a653f | ||
|
|
6339c3a7d7 | ||
|
|
8f7f0d4c61 | ||
|
|
74b4f51640 | ||
|
|
2caee79df4 | ||
|
|
34b4c39ccd | ||
|
|
34d1571669 | ||
|
|
22465b1b65 | ||
|
|
b91e8eab51 | ||
|
|
55e69bf263 | ||
|
|
02d3817cb1 | ||
|
|
fc9cdaabb4 | ||
|
|
2aa3d83402 | ||
|
|
fc5d6ada36 | ||
|
|
be599fb003 | ||
|
|
d79c4a13f6 | ||
|
|
e6fce14fde | ||
|
|
5c3a00b71b | ||
|
|
ced4d4d337 | ||
|
|
4a3ad9f4ff | ||
|
|
f5a72105ac | ||
|
|
0502eec0cd | ||
|
|
8e6933b5ca | ||
|
|
1fd1dec6cf | ||
|
|
422713ff53 | ||
|
|
ada45ee75b | ||
|
|
2f153686dd | ||
|
|
f691f1691e | ||
|
|
1f9e37b40c | ||
|
|
a1d2de1859 | ||
|
|
143472bdfc | ||
|
|
b9e64d42ff | ||
|
|
9a09bb8cda | ||
|
|
4b028af123 | ||
|
|
8033e5fd88 | ||
|
|
badf8c8f2f | ||
|
|
b6a353815c | ||
|
|
ea2f858dc9 | ||
|
|
324540009a | ||
|
|
1c1f90344f | ||
|
|
094dd45547 | ||
|
|
6d434722cc | ||
|
|
c8a4ed0c3d | ||
|
|
c8d997fbf0 | ||
|
|
911ed3a718 | ||
|
|
dbfed31432 | ||
|
|
6f3d4e224e | ||
|
|
4d6609990c | ||
|
|
5482196cc7 | ||
|
|
09f6c9ef81 | ||
|
|
9b8f97c041 | ||
|
|
c68c713a4b | ||
|
|
02e144f802 | ||
|
|
a9ab4d6c78 | ||
|
|
5df4e5761c | ||
|
|
6d8b8579ba | ||
|
|
eaa5190502 | ||
|
|
445db6a4d6 | ||
|
|
7f178b5f67 | ||
|
|
1208baf00e | ||
|
|
655a473cd5 | ||
|
|
e6c536930b | ||
|
|
f1510de360 | ||
|
|
cfac2768ae | ||
|
|
ab41f2e80e | ||
|
|
9ebabecfbe | ||
|
|
8914acff68 | ||
|
|
4a0ecba716 | ||
|
|
e99b1d4171 | ||
|
|
a59d5d3086 | ||
|
|
600a77da3a | ||
|
|
bc83f6fa22 | ||
|
|
4cbde7e7aa | ||
|
|
3ae63408e6 | ||
|
|
e740c6d162 | ||
|
|
f7f5852476 | ||
|
|
447e2ab264 | ||
|
|
8a77f22158 | ||
|
|
c096327be4 | ||
|
|
911153e1aa | ||
|
|
a60eadf8fc | ||
|
|
9ebbf2557f | ||
|
|
42b279d22a | ||
|
|
1a452db4cf | ||
|
|
c1cf5be616 | ||
|
|
dd146210ba | ||
|
|
94088f795c | ||
|
|
c5b09189df | ||
|
|
197c44bb07 | ||
|
|
c202a941b9 | ||
|
|
383594b03c | ||
|
|
42bfe572ef | ||
|
|
1677c47500 | ||
|
|
53a447c72d | ||
|
|
f23a891ece | ||
|
|
0e42c657b3 | ||
|
|
b17c15f702 | ||
|
|
c9cdf6e77d | ||
|
|
4f6ead842d | ||
|
|
adcddfa84d | ||
|
|
d67c5c5218 | ||
|
|
fb72010bdf | ||
|
|
8bc8bdb2f2 | ||
|
|
8f4cf9f2a0 | ||
|
|
ed323a8388 | ||
|
|
d559b437b3 | ||
|
|
6155306acc | ||
|
|
6f35d86890 | ||
|
|
cbf32a36c6 | ||
|
|
9789e0f198 | ||
|
|
574209c837 | ||
|
|
74c83fc5e1 | ||
|
|
a4ddc11bab | ||
|
|
3bf480234f | ||
|
|
29811ca5de | ||
|
|
b7522420cf | ||
|
|
f7d1e22e4e | ||
|
|
e0c38031b1 | ||
|
|
e046512e86 | ||
|
|
7cba0de268 | ||
|
|
425e476f20 | ||
|
|
e5dd33d5a1 | ||
|
|
07012615ff | ||
|
|
23f3ae51db | ||
|
|
10514b8039 | ||
|
|
19637fd706 | ||
|
|
dd56e2b42e | ||
|
|
e0b6530275 | ||
|
|
a0d3f3b745 | ||
|
|
438c13fe2d | ||
|
|
c067300ee8 | ||
|
|
020ca2f312 | ||
|
|
5ede1dd97d | ||
|
|
45f6ae214a | ||
|
|
45a9093055 | ||
|
|
d6c7a0b757 | ||
|
|
55181165c8 | ||
|
|
b6f7483bf9 | ||
|
|
0593c764b6 | ||
|
|
11a3bfd893 | ||
|
|
f1d7fc656c | ||
|
|
ec918b78ed | ||
|
|
6f5ee7c695 | ||
|
|
e4264d0199 | ||
|
|
9feefe8203 | ||
|
|
93f9539f7b | ||
|
|
8865f4946a | ||
|
|
65e13d7b50 | ||
|
|
11189bc605 | ||
|
|
40faaec31e | ||
|
|
b5b384f5da | ||
|
|
23c802bb5f | ||
|
|
4567f62ef8 | ||
|
|
cd00535212 | ||
|
|
3ff626e2aa | ||
|
|
deee63c701 | ||
|
|
b465b9fdaf | ||
|
|
f377f97931 | ||
|
|
58bee33383 | ||
|
|
18075e0686 | ||
|
|
b836774289 | ||
|
|
f2f15208bf | ||
|
|
164742387f | ||
|
|
e62b44bbbe | ||
|
|
e9d0491b42 | ||
|
|
3fc2127337 | ||
|
|
702d92d4f2 | ||
|
|
a440c22455 | ||
|
|
1c2e635650 | ||
|
|
7b856f32a2 | ||
|
|
42b96e042b | ||
|
|
f8023e9a72 | ||
|
|
7b9b78684c | ||
|
|
10d6fb8b2b | ||
|
|
1556b8930c | ||
|
|
d52bade085 | ||
|
|
2c98c8606c | ||
|
|
a8430c79eb | ||
|
|
3fe5fd89c4 | ||
|
|
78406b3110 | ||
|
|
2dfdbd6d78 | ||
|
|
2e86840e92 | ||
|
|
9600d5707c | ||
|
|
497cf333b0 | ||
|
|
eb55b9420a | ||
|
|
3b3e46983f | ||
|
|
31fb527218 | ||
|
|
685fef6b76 | ||
|
|
9a4eb22396 | ||
|
|
57eb3a77e7 | ||
|
|
dcc63046bd | ||
|
|
8388a7fed5 | ||
|
|
d5830399b7 | ||
|
|
38b8fa3b86 | ||
|
|
6f26c7e138 | ||
|
|
9e869ae69b | ||
|
|
4a7b0b20ad | ||
|
|
8fc971c07c | ||
|
|
cb884d97cb | ||
|
|
942db9df48 | ||
|
|
193f88b88f | ||
|
|
6785bccf0e | ||
|
|
579b9a685b | ||
|
|
57b3fd99bc | ||
|
|
c3ae98f3d1 | ||
|
|
d635815607 | ||
|
|
e524396aaf | ||
|
|
08edc04c6d | ||
|
|
e88d6265b4 | ||
|
|
5d91f033c0 | ||
|
|
771bd1545c | ||
|
|
f65fec66bb | ||
|
|
9c6dbeec69 | ||
|
|
563af71ed2 | ||
|
|
a4579c953a | ||
|
|
6a3c3de67c | ||
|
|
f8fa929166 | ||
|
|
6aaf532051 | ||
|
|
fb59bdfff4 | ||
|
|
c8aa1b0a19 | ||
|
|
11a201b3f5 | ||
|
|
b74741bccb | ||
|
|
b8739ace4f | ||
|
|
cd674ee9be | ||
|
|
8b3ed8171b | ||
|
|
28ee16279c | ||
|
|
ff43db0842 | ||
|
|
8ddac9acba | ||
|
|
0c2554da72 | ||
|
|
1274d56fce | ||
|
|
8d9e253f93 | ||
|
|
9166160008 | ||
|
|
35eb8fcdf9 | ||
|
|
d34c8b8bde | ||
|
|
80a079cbee | ||
|
|
5d181d1abb | ||
|
|
6fbd332b4d | ||
|
|
211e5952aa | ||
|
|
4078c61d6b | ||
|
|
8b7d1e6888 | ||
|
|
7c6daf2e11 | ||
|
|
e1f405d886 | ||
|
|
1d73956853 | ||
|
|
205f78151a | ||
|
|
5853eb01f5 | ||
|
|
1a5e71f852 | ||
|
|
7053e7b225 | ||
|
|
5bee26a8cc | ||
|
|
b9caff352b | ||
|
|
73089ce99c | ||
|
|
34080ae8b3 | ||
|
|
307c7d53f8 | ||
|
|
a6d2a6563b | ||
|
|
577e570c99 | ||
|
|
06411d4e2f | ||
|
|
3e287bfb2d | ||
|
|
9872fc9221 | ||
|
|
84d2db427c | ||
|
|
cfbf9187e6 | ||
|
|
5478ac05b8 | ||
|
|
836bd6bc40 | ||
|
|
1bab07fc5f | ||
|
|
4b3de044c3 | ||
|
|
f2c2fc6071 | ||
|
|
4dd52eb9dd | ||
|
|
e45fdc72a0 | ||
|
|
d67dd515c4 | ||
|
|
ff962aa5f0 | ||
|
|
c0e44b2c55 | ||
|
|
f0c239d625 | ||
|
|
58a31f435e | ||
|
|
c49439d541 | ||
|
|
0070cbfb1e | ||
|
|
7ed75fc83d | ||
|
|
13d701b0a7 | ||
|
|
07ed9c857b | ||
|
|
32ab53ba03 | ||
|
|
68344e981c | ||
|
|
10c1574a4a | ||
|
|
3a853e2ab1 | ||
|
|
0f639dc2c2 | ||
|
|
a801773545 | ||
|
|
ffbf4bb0db | ||
|
|
11e46155a1 | ||
|
|
f369b3ad00 | ||
|
|
6068666415 | ||
|
|
6a58e8a799 | ||
|
|
3bd2f69157 | ||
|
|
03ec0c9da1 | ||
|
|
47fc0e11eb | ||
|
|
27f4b3b3ae | ||
|
|
bb8c9a19cf | ||
|
|
16da6ae224 | ||
|
|
152b55f7f0 | ||
|
|
62d83151d7 | ||
|
|
9060a4b284 | ||
|
|
759de8e047 | ||
|
|
1e3dd61b57 | ||
|
|
d13bc8c313 | ||
|
|
9b6ef1f3ca | ||
|
|
173078dd8d | ||
|
|
ae2db6e204 | ||
|
|
5abea8c594 | ||
|
|
cb94d91e98 | ||
|
|
454e4b4c4b | ||
|
|
48a63c25d2 | ||
|
|
0fdc90523e | ||
|
|
3203308d82 | ||
|
|
83a7f5c562 | ||
|
|
55e0648fc8 | ||
|
|
b517b575da | ||
|
|
890b4098d7 | ||
|
|
11a0e3765b | ||
|
|
3ffb09c5e6 | ||
|
|
622b681109 | ||
|
|
c27a576b3d | ||
|
|
7bc8bdc13e | ||
|
|
0097a1ecbc | ||
|
|
525d57ec73 | ||
|
|
e3d7bb57ee | ||
|
|
095518e858 | ||
|
|
ac470f64c1 | ||
|
|
30215b0dac | ||
|
|
563252c823 | ||
|
|
3a7ffe95e8 | ||
|
|
b588a4ee02 | ||
|
|
ab0611fabc | ||
|
|
a121bb5f26 | ||
|
|
2fc01e0345 | ||
|
|
650c3657a5 | ||
|
|
d836eb515e | ||
|
|
717bc9fec2 | ||
|
|
1ad6da86b1 | ||
|
|
54680a5692 | ||
|
|
6ef5e79b39 | ||
|
|
1dbf51b406 | ||
|
|
8a3aa0d06e | ||
|
|
ca99efa215 | ||
|
|
cc202ce4ca | ||
|
|
ad44fe2335 | ||
|
|
452242f208 | ||
|
|
ff83af2e0d | ||
|
|
aacecc8517 | ||
|
|
a9a3eef157 | ||
|
|
7fc37c21fd | ||
|
|
ff828b483b | ||
|
|
64a9cce837 | ||
|
|
caa7ef429c | ||
|
|
38818d8222 | ||
|
|
fd7d2232a1 | ||
|
|
b5eb59344c | ||
|
|
3093e4906c | ||
|
|
2faddc1133 | ||
|
|
a2ead56593 | ||
|
|
03adc1f9f0 | ||
|
|
7bc707bcaa | ||
|
|
7ae74e3000 | ||
|
|
94da3b37b6 | ||
|
|
c6689228d0 | ||
|
|
d847da9d61 | ||
|
|
6637ea09bf | ||
|
|
99959470e2 | ||
|
|
826162ba9f | ||
|
|
546ccd66b3 | ||
|
|
628b15a3b5 | ||
|
|
72281233e1 | ||
|
|
c68722e420 | ||
|
|
901a4520fd | ||
|
|
3e083b907d | ||
|
|
3c4b25ecee | ||
|
|
c23bbb9e02 | ||
|
|
94897e9744 | ||
|
|
d722c4972a | ||
|
|
e93df8273b | ||
|
|
a1c65cff75 | ||
|
|
18bc7bf302 | ||
|
|
17bb442c78 | ||
|
|
3134a9ad42 | ||
|
|
eda96af607 | ||
|
|
038069f6e6 |
@@ -12,10 +12,11 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 14.0.2
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 16.0.1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 14.0.2
|
||||
distribution: 'adopt'
|
||||
java-version: '16.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
@@ -26,8 +27,16 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
path: build/jpackage/*
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = git@github.com:sparrowwallet/drongo.git
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
|
||||
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
|
||||
@@ -2,17 +2,29 @@
|
||||
|
||||
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).
|
||||
|
||||

|
||||
|
||||
## Building
|
||||
|
||||
To clone this project, use
|
||||
To clone this project, use
|
||||
|
||||
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
|
||||
or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 16 to be installed.
|
||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
`sudo apt install -y rpm fakeroot binutils`
|
||||
|
||||
|
||||
The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
|
||||
@@ -20,19 +32,21 @@ When updating to the latest HEAD
|
||||
|
||||
`git pull --recurse-submodules`
|
||||
|
||||
All jar files created are reproducible builds.
|
||||
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.
|
||||
|
||||
## Running
|
||||
|
||||
If you prefer to run Sparrow directly from source, it can be launched with
|
||||
If you prefer to run Sparrow directly from source, it can be launched from within the project directory with
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 14 must be installed.
|
||||
Java 16 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
Sparrow has a number of command line options, for example to change it's home folder or use testnet:
|
||||
Sparrow has a number of command line options, for example to change its home folder or use testnet:
|
||||
|
||||
```
|
||||
./sparrow -h
|
||||
@@ -43,20 +57,23 @@ Usage: sparrow [options]
|
||||
Path to Sparrow home folder
|
||||
--help, -h
|
||||
Show usage
|
||||
--level, -l
|
||||
Set log level
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest]
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet or regtest) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
A final fallback which can be useful when running the Sparrow binary is to create a file called ``network-testnet`` in the Sparrow home folder (see below) to configure the testnet network.
|
||||
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to running on testnet configuration as well.
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to be running on testnet configuration as well.
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores it's 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
|
||||
-------- | --------
|
||||
@@ -64,7 +81,7 @@ OSX | ~/.sparrow
|
||||
Linux | ~/.sparrow
|
||||
Windows | %APPDATA%/Sparrow
|
||||
|
||||
Testnet and regtest configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
@@ -74,6 +91,12 @@ Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab abo
|
||||
|
||||
Sparrow is licensed under the Apache 2 software licence.
|
||||
|
||||
## GPG Key
|
||||
|
||||
The Sparrow release binaries here and on [sparrowwallet.com](https://sparrowwallet.com/download/) are signed using [craigraw's GPG key](https://keybase.io/craigraw):
|
||||
Fingerprint: D4D0D3202FC06849A257B38DE94618334C674B40
|
||||
64-bit: E946 1833 4C67 4B40
|
||||
|
||||
## Credit
|
||||
|
||||

|
||||
|
||||
+444
-28
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.9'
|
||||
id 'org.kordamp.gradle.jdeps' version '0.9.0'
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.10'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '0.9.9'
|
||||
def sparrowVersion = '1.6.1'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -20,7 +20,7 @@ repositories {
|
||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'http://maven.openimaj.org/' }
|
||||
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
@@ -29,7 +29,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "15"
|
||||
version = "17"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -38,29 +38,47 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo')) {
|
||||
exclude group: 'org.hamcrest'
|
||||
exclude group: 'junit'
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:2.0.206')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
}
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.5.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}: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.0.2' ) {
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
||||
@@ -69,10 +87,23 @@ dependencies {
|
||||
exclude group: 'org.openjfx', module: 'javafx-web'
|
||||
exclude group: 'org.openjfx', module: 'javafx-media'
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.32')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.with {
|
||||
@@ -91,8 +122,13 @@ processResources {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
@@ -100,6 +136,7 @@ run {
|
||||
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
@@ -107,10 +144,11 @@ run {
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=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"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
}
|
||||
}
|
||||
@@ -122,12 +160,17 @@ jlink {
|
||||
requires 'java.xml'
|
||||
requires 'java.logging'
|
||||
requires 'javafx.base'
|
||||
requires 'com.fasterxml.jackson.databind'
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
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.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']
|
||||
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/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -138,32 +181,44 @@ jlink {
|
||||
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
|
||||
"--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=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
|
||||
"--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.mac=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
|
||||
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
jpackage {
|
||||
imageName = "Sparrow"
|
||||
installerName = "Sparrow"
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/associations.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerType = "exe"
|
||||
}
|
||||
if(os.linux) {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
@@ -173,3 +228,364 @@ jlink {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
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) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
|
||||
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')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
requires('java.desktop')
|
||||
requires('com.nativelibs4java.bridj')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
|
||||
exports('org.slf4j.bridge')
|
||||
requires('java.logging')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
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.8.jar', 'logback.classic', '1.2.8') {
|
||||
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.event')
|
||||
exports('org.fxmisc.richtext.model')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.fxmisc.flowless')
|
||||
requires('org.reactfx.reactfx')
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
||||
exports('org.fxmisc.flowless')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
||||
exports('org.reactfx')
|
||||
exports('org.reactfx.value')
|
||||
exports('org.reactfx.collection')
|
||||
exports('org.reactfx.util')
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
|
||||
exports('io.reactivex.rxjavafx.schedulers')
|
||||
requires('io.reactivex.rxjava2')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
|
||||
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.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
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')
|
||||
}
|
||||
module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') {
|
||||
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')
|
||||
requires('io.reactivex.rxjava2')
|
||||
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.wallet.bip47.rpc')
|
||||
exports('com.samourai.wallet.bip47.rpc.java')
|
||||
exports('com.samourai.wallet.cahoots')
|
||||
exports('com.samourai.wallet.cahoots.psbt')
|
||||
exports('com.samourai.wallet.cahoots.stonewallx2')
|
||||
exports('com.samourai.soroban.cahoots')
|
||||
exports('com.samourai.soroban.client')
|
||||
exports('com.samourai.soroban.client.cahoots')
|
||||
exports('com.samourai.soroban.client.meeting')
|
||||
exports('com.samourai.soroban.client.rpc')
|
||||
exports('com.samourai.wallet.send')
|
||||
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.chain')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
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')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
||||
exports('com.google.protobuf')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module("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.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('junit-4.12.jar', 'junit', '4.12') {
|
||||
exports('org.junit')
|
||||
}
|
||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin' // so we can assign and ID to our plugin
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
// here we register our plugin with an ID
|
||||
register("extra-java-module-info") {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Entry point of our plugin that should be applied in the root project.
|
||||
*/
|
||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||
|
||||
// setup the transform for all projects in the build
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||
}
|
||||
|
||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||
|
||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||
c -> c.getAttributes().attribute(javaModule, true));
|
||||
|
||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||
|
||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||
t.parameters(p -> {
|
||||
p.setModuleInfo(extension.getModuleInfo());
|
||||
p.setAutomaticModules(extension.getAutomaticModules());
|
||||
});
|
||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||
if (!configuration.isCanBeResolved()) {
|
||||
return false;
|
||||
}
|
||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.ModuleVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.jar.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* An artifact transform that applies additional information to Jars without module information.
|
||||
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||
*/
|
||||
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||
|
||||
public static class Parameter implements TransformParameters, Serializable {
|
||||
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||
|
||||
@Input
|
||||
public Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
|
||||
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||
this.automaticModules = automaticModules;
|
||||
}
|
||||
}
|
||||
|
||||
@InputArtifact
|
||||
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
} else if (isAutoModule(originalJar)) {
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
## Sparrow Wallet Repository Docs
|
||||
|
||||
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
|
||||
The documentation here is mainly developer-related resources.
|
||||
|
||||
### [Reproducible builds](reproducible.md)
|
||||
|
||||
Documentation to create and verify a build of the project against the released binaries.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
## Reproducible builds
|
||||
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
||||
|
||||
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
Work on resolving both of these issues is ongoing.
|
||||
|
||||
### Reproducing a release
|
||||
|
||||
#### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
|
||||
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
|
||||
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt-get install -y wget apt-transport-https gnupg
|
||||
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
|
||||
rm adoptopenjdk-keyring.gpg
|
||||
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
|
||||
```
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 16.0.1.hs-adpt
|
||||
```
|
||||
|
||||
#### Other requirements
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt install -y rpm fakeroot binutils
|
||||
```
|
||||
|
||||
#### Building the binaries
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.5.2"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
|
||||
```shell
|
||||
cd sparrow
|
||||
./gradlew jpackage
|
||||
```
|
||||
|
||||
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
||||
|
||||
#### Verifying the binaries are identical
|
||||
|
||||
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`).
|
||||
Then compare all of the folders and files recursively:
|
||||
|
||||
```shell
|
||||
diff -r build/jpackage/Sparrow /tmp/Sparrow
|
||||
```
|
||||
|
||||
This command should have no output indicating that the two folders (and all their contents) are identical.
|
||||
|
||||
+1
-1
Submodule drongo updated: 0567409742...04631be8c1
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
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
|
||||
|
||||
Vendored
+4
-18
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,28 +64,14 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd `dirname $0`
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
extension=psbt
|
||||
mime-type=application/octet-stream
|
||||
description=Partially Signed Bitcoin Transaction
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/bitcoin
|
||||
description=Bitcoin Scheme URI
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Sparrow
|
||||
Comment=Sparrow
|
||||
Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.9.9</string>
|
||||
<string>1.6.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -30,10 +30,85 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>100</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (C) 2020</string>
|
||||
<string>Copyright (C) 2021</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.bitcoin</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bitcoin</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.bitcoin.psbt</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.bitcoin.txn</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
|
||||
<?ifdef JpIsSystemWide ?>
|
||||
<?define JpInstallScope="perMachine"?>
|
||||
<?else?>
|
||||
<?define JpInstallScope="perUser"?>
|
||||
<?endif?>
|
||||
|
||||
<?define JpProductLanguage=1033 ?>
|
||||
<?define JpInstallerVersion=200 ?>
|
||||
<?define JpCompressedMsi=yes ?>
|
||||
|
||||
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
||||
|
||||
<Product
|
||||
Id="$(var.JpProductCode)"
|
||||
Name="$(var.JpAppName)"
|
||||
Language="$(var.JpProductLanguage)"
|
||||
Version="$(var.JpAppVersion)"
|
||||
Manufacturer="$(var.JpAppVendor)"
|
||||
UpgradeCode="$(var.JpProductUpgradeCode)">
|
||||
|
||||
<Package
|
||||
Description="$(var.JpAppDescription)"
|
||||
Manufacturer="$(var.JpAppVendor)"
|
||||
InstallerVersion="$(var.JpInstallerVersion)"
|
||||
Compressed="$(var.JpCompressedMsi)"
|
||||
InstallScope="$(var.JpInstallScope)" Platform="x64"
|
||||
/>
|
||||
|
||||
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="no"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="yes"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="no"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="yes"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<!-- Standard required root -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir"/>
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
|
||||
<ComponentGroupRef Id="Shortcuts"/>
|
||||
<ComponentGroupRef Id="Files"/>
|
||||
<ComponentGroupRef Id="FileAssociations"/>
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
</Feature>
|
||||
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
|
||||
<?endif?>
|
||||
|
||||
<UI>
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
|
||||
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
|
||||
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
|
||||
<Text>!(loc.message.install.dir.exist)</Text>
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<!--
|
||||
Run WixUI_InstallDir dialog in the default install directory.
|
||||
-->
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
|
||||
|
||||
<?ifndef JpLicenseRtf ?>
|
||||
<!--
|
||||
No license file provided.
|
||||
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
|
||||
to exclude license dialog.
|
||||
-->
|
||||
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
|
||||
<?endif?>
|
||||
|
||||
<?else?>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<UIRef Id="WixUI_Minimal" />
|
||||
<?endif?>
|
||||
|
||||
<?endif?>
|
||||
</UI>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
|
||||
<?endif?>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/psbt
|
||||
extension=psbt
|
||||
description=Partially Signed Bitcoin Transaction
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/bitcoin-transaction
|
||||
extension=txn
|
||||
description=Bitcoin Transaction
|
||||
@@ -12,7 +12,7 @@ public class AboutController {
|
||||
private Label title;
|
||||
|
||||
public void initializeView() {
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
@@ -22,4 +22,8 @@ public class AboutController {
|
||||
public void close(ActionEvent event) {
|
||||
stage.close();
|
||||
}
|
||||
|
||||
public void openDonate(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/donate");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
public class Args {
|
||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||
@@ -10,6 +11,9 @@ public class Args {
|
||||
@Parameter(names = { "--network", "-n" }, description = "Network to use")
|
||||
public Network network;
|
||||
|
||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||
public Level level;
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,53 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.control.WelcomeDialog;
|
||||
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.FileType;
|
||||
import com.sparrowwallet.sparrow.io.IOUtils;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
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 {
|
||||
private static final Logger log = LoggerFactory.getLogger(MainApp.class);
|
||||
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "0.9.9";
|
||||
public static final String APP_VERSION = "1.6.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 Stage mainStage;
|
||||
|
||||
private static SparrowInstance sparrowInstance;
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -51,10 +59,12 @@ public class MainApp extends Application {
|
||||
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(getHostServices());
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent()) {
|
||||
mode = optionalMode.get();
|
||||
@@ -64,11 +74,29 @@ public class MainApp extends Application {
|
||||
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);
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.initialize(this);
|
||||
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) {
|
||||
@@ -77,9 +105,8 @@ public class MainApp extends Application {
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList());
|
||||
Collections.reverse(encryptedWalletFiles);
|
||||
//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);
|
||||
@@ -91,12 +118,20 @@ public class MainApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -108,9 +143,13 @@ public class MainApp extends Application {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
log.info("Using configured Sparrow home folder of " + args.dir);
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
@@ -121,7 +160,7 @@ public class MainApp extends Application {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase()));
|
||||
} catch(Exception e) {
|
||||
log.warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,10 +170,61 @@ public class MainApp extends Application {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
log.info("Using " + Network.get() + " configuration");
|
||||
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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
|
||||
public class WelcomeController {
|
||||
@FXML
|
||||
private VBox welcomeBox;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private StatusBar serverStatus;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
public void initializeView() {
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
welcomeBox.getStyleClass().add("offline");
|
||||
serverStatus.setText("Offline");
|
||||
serverToggle.addEventFilter(MouseEvent.MOUSE_RELEASED, Event::consume);
|
||||
Tooltip tooltip = new Tooltip("Demonstration only - you are not connected!");
|
||||
tooltip.setShowDelay(Duration.ZERO);
|
||||
serverToggle.setTooltip(tooltip);
|
||||
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
|
||||
});
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("public-electrum");
|
||||
PauseTransition wait = new PauseTransition(Duration.millis(200));
|
||||
wait.setOnFinished((e) -> {
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Public Server (demonstration only)");
|
||||
});
|
||||
wait.play();
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("bitcoin-core");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("private-electrum");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean back() {
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("offline");
|
||||
PauseTransition wait = new PauseTransition(Duration.millis(200));
|
||||
wait.setOnFinished((e) -> {
|
||||
serverToggle.setSelected(false);
|
||||
serverStatus.setText("Offline");
|
||||
});
|
||||
wait.play();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("public-electrum");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Public Server (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step4.isVisible()) {
|
||||
step4.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("bitcoin-core");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class WelcomeDialog extends Dialog<Mode> {
|
||||
public WelcomeDialog() {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
|
||||
dialogPane.setContent(welcomeLoader.load());
|
||||
WelcomeController welcomeController = welcomeLoader.getController();
|
||||
welcomeController.initializeView();
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
|
||||
Button onlineButton = (Button)dialogPane.lookupButton(onlineButtonType);
|
||||
Button offlineButton = (Button)dialogPane.lookupButton(offlineButtonType);
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
onlineButton.managedProperty().bind(onlineButton.visibleProperty());
|
||||
offlineButton.managedProperty().bind(offlineButton.visibleProperty());
|
||||
|
||||
backButton.setDisable(true);
|
||||
onlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
offlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!welcomeController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
onlineButton.setDefaultButton(true);
|
||||
}
|
||||
backButton.setDisable(false);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
backButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
nextButton.setVisible(true);
|
||||
if(!welcomeController.back()) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
private boolean discoverAccounts = false;
|
||||
|
||||
public AddAccountDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Add Account");
|
||||
dialogPane.setHeaderText("Choose an account to add:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
|
||||
key.setFontSize(50);
|
||||
dialogPane.setGraphic(key);
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.setPrefHeight(50);
|
||||
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
}
|
||||
}
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
availableAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverAccounts = true;
|
||||
});
|
||||
}
|
||||
|
||||
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
|
||||
standardAccountCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(StandardAccount account) {
|
||||
if(account == null) {
|
||||
return "None Available";
|
||||
}
|
||||
|
||||
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
|
||||
return "Whirlpool Accounts";
|
||||
}
|
||||
|
||||
return account.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandardAccount fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if(standardAccountCombo.getItems().isEmpty()) {
|
||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.setDisable(true);
|
||||
} else {
|
||||
standardAccountCombo.getSelectionModel().select(0);
|
||||
}
|
||||
content.getChildren().add(standardAccountCombo);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null));
|
||||
}
|
||||
|
||||
public boolean isDiscoverAccounts() {
|
||||
return discoverAccounts;
|
||||
}
|
||||
}
|
||||
@@ -3,62 +3,55 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class AddressCell extends TreeTableCell<Entry, Entry> {
|
||||
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
public AddressCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.RIGHT);
|
||||
getStyleClass().add("address-cell");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
|
||||
super.updateItem(addressStatus, empty);
|
||||
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
getStyleClass().add("address-cell");
|
||||
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
|
||||
EntryCell.applyRowStyles(this, utxoEntry);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if(entry instanceof UtxoEntry) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
Address address = utxoEntry.getAddress();
|
||||
if(utxoEntry != null) {
|
||||
Address address = addressStatus.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(getTooltipText(utxoEntry));
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(utxoEntry.isDuplicateAddress()) {
|
||||
if(addressStatus.isDuplicate()) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(getTooltipText(utxoEntry));
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry) {
|
||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
|
||||
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class AddressStringConverter extends StringConverter<Address> {
|
||||
@Override
|
||||
public Address fromString(String value) {
|
||||
// If the specified value is null or zero-length, return null
|
||||
if(value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if (value.length() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Address.fromString(value);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address value) {
|
||||
// If the specified value is null, return a zero-length String
|
||||
if(value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
@@ -33,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
addressCol.setSortable(false);
|
||||
getColumns().add(addressCol);
|
||||
|
||||
if(address != null) {
|
||||
if(address != null && !rootEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
}
|
||||
|
||||
@@ -67,20 +68,26 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
setOnMouseClicked(mouseEvent -> {
|
||||
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
||||
if(mouseEvent.getClickCount() == 2) {
|
||||
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
|
||||
if(treeItem != null && treeItem.getChildren().isEmpty()) {
|
||||
Entry entry = getSelectionModel().getSelectedItem().getValue();
|
||||
if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
setOnMouseClicked(mouseEvent -> {
|
||||
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
||||
if(mouseEvent.getClickCount() == 2) {
|
||||
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
|
||||
if(treeItem != null && treeItem.getChildren().isEmpty()) {
|
||||
Entry entry = getSelectionModel().getSelectedItem().getValue();
|
||||
if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,22 +106,21 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
//We only ever add or replace 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
|
||||
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
||||
|
||||
for(WalletNode updatedNode : updatedNodes) {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
|
||||
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
|
||||
if(optEntry.isPresent()) {
|
||||
int index = rootEntry.getChildren().indexOf(optEntry.get());
|
||||
rootEntry.getChildren().set(index, nodeEntry);
|
||||
NodeEntry existingEntry = (NodeEntry)optEntry.get();
|
||||
existingEntry.refreshChildren();
|
||||
} else {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
}
|
||||
}
|
||||
|
||||
sort();
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
|
||||
@@ -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,26 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class BalanceChart extends LineChart<Number, Number> {
|
||||
private static final int MAX_VALUES = 500;
|
||||
|
||||
private XYChart.Series<Number, Number> balanceSeries;
|
||||
|
||||
private TransactionEntry selectedEntry;
|
||||
@@ -37,7 +43,7 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
|
||||
balanceSeries.getData().clear();
|
||||
|
||||
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
|
||||
List<Data<Number, Number>> balanceDataList = getTransactionEntries(walletTransactionsEntry)
|
||||
.map(entry -> (TransactionEntry)entry)
|
||||
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
|
||||
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
|
||||
@@ -74,6 +80,24 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<Entry> getTransactionEntries(WalletTransactionsEntry walletTransactionsEntry) {
|
||||
int total = walletTransactionsEntry.getChildren().size();
|
||||
if(walletTransactionsEntry.getChildren().size() <= MAX_VALUES) {
|
||||
return walletTransactionsEntry.getChildren().stream();
|
||||
}
|
||||
|
||||
int bucketSize = total / MAX_VALUES;
|
||||
List<List<Entry>> buckets = Lists.partition(walletTransactionsEntry.getChildren(), bucketSize);
|
||||
List<Entry> reducedEntries = new ArrayList<>(MAX_VALUES);
|
||||
for(List<Entry> bucket : buckets) {
|
||||
long max = bucket.stream().mapToLong(entry -> Math.abs(entry.getValue())).max().orElse(0);
|
||||
Entry bucketEntry = bucket.stream().filter(entry -> entry.getValue() == max || entry.getValue() == -max).findFirst().orElseThrow();
|
||||
reducedEntries.add(bucketEntry);
|
||||
}
|
||||
|
||||
return reducedEntries.stream();
|
||||
}
|
||||
|
||||
public void select(TransactionEntry transactionEntry) {
|
||||
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
|
||||
for(Node selectedSymbol : selectedSymbols) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
@@ -27,9 +28,11 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||
|
||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||
Integer targetBlocks = targetBlocksIter.next();
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedTargetBlocks != null) {
|
||||
|
||||
@@ -10,16 +10,26 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final Tooltip tooltip;
|
||||
|
||||
public CoinCell() {
|
||||
super();
|
||||
tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,7 +48,8 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
|
||||
final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
@@ -66,6 +77,10 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
if(amount.longValue() < 0) {
|
||||
getStyleClass().add("negative-amount");
|
||||
}
|
||||
} else if(entry instanceof UtxoEntry) {
|
||||
setGraphic(null);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -15,7 +16,7 @@ import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CoinLabel extends CopyableLabel {
|
||||
public class CoinLabel extends Label {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
@@ -28,7 +29,6 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
public CoinLabel(String text) {
|
||||
super(text);
|
||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
@@ -77,7 +77,7 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
|
||||
@@ -2,12 +2,27 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
@@ -51,10 +66,45 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
} else {
|
||||
setPlaceholder(new Label("No transactions"));
|
||||
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Node getDefaultPlaceholder(Wallet wallet) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
||||
Optional<Date> optDate = dlg.showAndWait();
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet pastWallet = wallet.copy();
|
||||
wallet.setBirthDate(optDate.get());
|
||||
//Trigger background save of birthdate
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
hyperlink.setText("Scan for previous transactions?");
|
||||
} else {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
|
||||
hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?");
|
||||
}
|
||||
|
||||
stackPane.getChildren().add(hyperlink);
|
||||
}
|
||||
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
return stackPane;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
public class ComboBoxTextField extends CustomTextField {
|
||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private boolean initialized;
|
||||
private boolean comboShowing;
|
||||
|
||||
public ComboBoxTextField() {
|
||||
super();
|
||||
getStyleClass().add("combo-text-field");
|
||||
setupComboButtonField(super.rightProperty());
|
||||
|
||||
disabledProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(comboProperty.isNotNull().get()) {
|
||||
comboProperty.get().setVisible(!newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupComboButtonField(ObjectProperty<Node> rightProperty) {
|
||||
Region showComboButton = new Region();
|
||||
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
|
||||
StackPane showComboButtonPane = new StackPane(showComboButton);
|
||||
showComboButtonPane.getStyleClass().addAll("combo-button"); //$NON-NLS-1$
|
||||
showComboButtonPane.setCursor(Cursor.DEFAULT);
|
||||
showComboButtonPane.setOnMouseReleased(e -> {
|
||||
if(comboProperty.isNotNull().get()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rightProperty.set(showComboButtonPane);
|
||||
}
|
||||
|
||||
public ComboBox<?> getComboProperty() {
|
||||
return comboProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<ComboBox<?>> walletComboProperty() {
|
||||
return comboProperty;
|
||||
}
|
||||
|
||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,10 @@ public class ConfirmationProgressIndicator extends StackPane {
|
||||
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
|
||||
sequence.getChildren().add(upTickLineTimeline);
|
||||
|
||||
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(10), confirmationGroup);
|
||||
groupFadeOut.setFromValue(1);
|
||||
groupFadeOut.setToValue(0);
|
||||
Timeline groupFadeOut = AnimationUtil.getSlowFadeOut(confirmationGroup, Duration.minutes(10), 1.0, 10);
|
||||
sequence.getChildren().add(groupFadeOut);
|
||||
|
||||
confirmationsProperty().unbind();
|
||||
}
|
||||
|
||||
sequence.play();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
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 java.util.Locale;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
public CopyableCoinLabel() {
|
||||
this("Unknown");
|
||||
}
|
||||
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
|
||||
public final LongProperty valueProperty() {
|
||||
return valueProperty;
|
||||
}
|
||||
|
||||
public final long getValue() {
|
||||
return valueProperty.get();
|
||||
}
|
||||
|
||||
public final void setValue(long value) {
|
||||
this.valueProperty.set(value);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refresh(Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void refresh(BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), bitcoinUnit);
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue);
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setText(btcValue);
|
||||
setText(satsValue);
|
||||
}
|
||||
}
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Long.toString(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
@@ -16,10 +22,36 @@ import org.controlsfx.control.textfield.CustomTextField;
|
||||
public class CopyableTextField extends CustomTextField {
|
||||
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() {
|
||||
super();
|
||||
getStyleClass().add("copyable-text-field");
|
||||
setupCopyButtonField(super.rightProperty());
|
||||
editableProperty().addListener((observable, oldValue, editable) -> {
|
||||
if(!editable) {
|
||||
setOnMouseClicked(copyHandler);
|
||||
selectedTextProperty().addListener(selectionListener);
|
||||
} else {
|
||||
setOnMouseClicked(null);
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
@@ -31,7 +63,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
copyButtonPane.setCursor(Cursor.DEFAULT);
|
||||
copyButtonPane.setOnMouseReleased(e -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getText());
|
||||
content.putString(getCopyText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
@@ -61,4 +93,8 @@ public class CopyableTextField extends CustomTextField {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String getCopyText() {
|
||||
return getText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -34,14 +35,19 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
if(entry instanceof UtxoEntry) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||
setText("Unconfirmed " + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
|
||||
} else {
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
||||
setContextMenu(null);
|
||||
} else if(utxoEntry.getHashIndex().getDate() != null) {
|
||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||
setText(date);
|
||||
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
|
||||
} else {
|
||||
setText("Unknown");
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
int height = utxoEntry.getHashIndex().getHeight();
|
||||
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
|
||||
setTooltip(tooltip);
|
||||
|
||||
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
public static String getShortDateFormat(Date date) {
|
||||
if(date == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
long elapsed = (now.getTime() - date.getTime()) / 1000;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class DateStringConverter extends StringConverter<LocalDate> {
|
||||
public static final String FORMAT_PATTERN = "yyyy/MM/dd";
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN);
|
||||
|
||||
@Override
|
||||
public String toString(LocalDate date) {
|
||||
if (date != null) {
|
||||
return DATE_FORMATTER.format(date);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate fromString(String string) {
|
||||
if (string != null && !string.isEmpty()) {
|
||||
return LocalDate.parse(string, DATE_FORMATTER);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
@@ -79,7 +80,7 @@ public class DescriptorArea extends CodeArea {
|
||||
copyOutputDescriptor.setOnAction(AE -> {
|
||||
hide();
|
||||
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);
|
||||
});
|
||||
getItems().add(copyOutputDescriptor);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
private final Wallet wallet;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
|
||||
public DeviceAddressDialog(List<String> operationFingerprints, Wallet wallet, KeyDerivation keyDerivation) {
|
||||
super(operationFingerprints);
|
||||
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
||||
this.wallet = wallet;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -25,8 +25,8 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, keyDerivation, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(wallet, outputDescriptor, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Objects;
|
||||
public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
private final List<String> operationFingerprints;
|
||||
private final Accordion deviceAccordion;
|
||||
private final Button scanButton;
|
||||
private final VBox scanBox;
|
||||
private final Label scanLabel;
|
||||
|
||||
@@ -57,18 +58,19 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||
usb.setFontSize(50);
|
||||
scanLabel = new Label("Connect Hardware Wallet");
|
||||
Button button = new Button("Scan...");
|
||||
button.setPrefSize(120, 60);
|
||||
button.setOnAction(event -> {
|
||||
scanButton = new Button("Scan...");
|
||||
scanButton.setPrefSize(120, 60);
|
||||
scanButton.setOnAction(event -> {
|
||||
scan();
|
||||
});
|
||||
scanBox.getChildren().addAll(usb, scanLabel, button);
|
||||
scanBox.getChildren().addAll(usb, scanLabel, scanButton);
|
||||
scanBox.managedProperty().bind(scanBox.visibleProperty());
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
} else {
|
||||
Platform.runLater(() -> setDevices(devices));
|
||||
@@ -89,19 +91,29 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(360);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
scanButton.setText("Scan...");
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
setDevices(devices);
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
scanButton.setText("Scan...");
|
||||
deviceAccordion.getPanes().clear();
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
|
||||
});
|
||||
enumerateService.setOnRunning(workerStateEvent -> {
|
||||
scanButton.setText("Scanning...");
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
|
||||
@@ -111,23 +123,24 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
if(operationFingerprints != null) {
|
||||
dialogDevices.removeIf(device -> {
|
||||
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.getNeedsPinSent() || device.getNeedsPassphraseSent());
|
||||
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.isNeedsPinSent() || device.isNeedsPassphraseSent());
|
||||
});
|
||||
}
|
||||
|
||||
deviceAccordion.getPanes().clear();
|
||||
|
||||
if(dialogDevices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
scanLabel.setText("No matching devices found");
|
||||
} else {
|
||||
scanBox.setVisible(false);
|
||||
for(Device device : dialogDevices) {
|
||||
DevicePane devicePane = getDevicePane(device);
|
||||
DevicePane devicePane = getDevicePane(device, dialogDevices.size() == 1);
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract DevicePane getDevicePane(Device device);
|
||||
protected abstract DevicePane getDevicePane(Device device, boolean defaultDevice);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
|
||||
private final Wallet masterWallet;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
|
||||
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
|
||||
super(operationFingerprints);
|
||||
this.masterWallet = masterWallet;
|
||||
this.availableAccounts = availableAccounts;
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
|
||||
setResult(event.getDiscoveredKeystores());
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,22 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -25,8 +26,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -35,7 +34,8 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||
@@ -43,29 +43,37 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private final DeviceOperation deviceOperation;
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final String message;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
private final Device device;
|
||||
|
||||
private CustomPasswordField pinField;
|
||||
private Button unlockButton;
|
||||
private Button enterPinButton;
|
||||
private Button setPassphraseButton;
|
||||
private SplitMenuButton importButton;
|
||||
private ButtonBase importButton;
|
||||
private Button signButton;
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
private Button discoverKeystoresButton;
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
|
||||
public DevicePane(Wallet wallet, Device device) {
|
||||
private boolean defaultDevice;
|
||||
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = requiredDerivation;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -73,30 +81,22 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
createSetPassphraseButton();
|
||||
createImportButton();
|
||||
|
||||
if(device.getError() != null) {
|
||||
setError("Error", device.getError());
|
||||
Platform.runLater(() -> {
|
||||
setExpanded(true);
|
||||
});
|
||||
} else if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
}
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||
}
|
||||
|
||||
public DevicePane(PSBT psbt, Device device) {
|
||||
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = null;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -104,25 +104,22 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
createSetPassphraseButton();
|
||||
createSignButton();
|
||||
|
||||
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
}
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) {
|
||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -130,25 +127,22 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
createSetPassphraseButton();
|
||||
createDisplayAddressButton();
|
||||
|
||||
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
}
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = message;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -156,15 +150,48 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
createSetPassphraseButton();
|
||||
createSignMessageButton();
|
||||
|
||||
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
this.availableAccounts = availableAccounts;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
createSetPassphraseButton();
|
||||
createDiscoverKeystoresButton();
|
||||
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||
}
|
||||
|
||||
private void initialise(Device device) {
|
||||
if(device.isNeedsPinSent()) {
|
||||
unlockButton.setDefaultButton(defaultDevice);
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
} else if(device.isNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
} else if(device.getError() != null) {
|
||||
setError("Error", device.getError());
|
||||
Platform.runLater(() -> {
|
||||
setExpanded(true);
|
||||
});
|
||||
} else {
|
||||
showOperationButton();
|
||||
}
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,7 +201,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void setDefaultStatus() {
|
||||
setDescription(device.getNeedsPinSent() ? "Locked" : device.getNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
||||
}
|
||||
|
||||
private void createUnlockButton() {
|
||||
@@ -201,23 +228,40 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation());
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
|
||||
});
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
|
||||
if(importButton instanceof SplitMenuButton importMenuButton) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
@@ -225,7 +269,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void createSignButton() {
|
||||
signButton = new Button("Sign");
|
||||
signButton.setDefaultButton(true);
|
||||
signButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
signButton.setMinWidth(44);
|
||||
signButton.setOnAction(event -> {
|
||||
@@ -238,7 +281,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void createDisplayAddressButton() {
|
||||
displayAddressButton = new Button("Display Address");
|
||||
displayAddressButton.setDefaultButton(true);
|
||||
displayAddressButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
displayAddressButton.setOnAction(event -> {
|
||||
displayAddressButton.setDisable(true);
|
||||
@@ -247,14 +289,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
|
||||
displayAddressButton.setVisible(false);
|
||||
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
|
||||
List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
|
||||
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
|
||||
displayAddressButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void createSignMessageButton() {
|
||||
signMessageButton = new Button("Sign Message");
|
||||
signMessageButton.setDefaultButton(true);
|
||||
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
signMessageButton.setOnAction(event -> {
|
||||
signMessageButton.setDisable(true);
|
||||
@@ -268,6 +310,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void createDiscoverKeystoresButton() {
|
||||
discoverKeystoresButton = new Button("Discover");
|
||||
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
discoverKeystoresButton.setOnAction(event -> {
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystores();
|
||||
});
|
||||
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
|
||||
discoverKeystoresButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -278,8 +331,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setMaxHeight(120);
|
||||
vBox.setSpacing(42);
|
||||
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
pinField = new ViewPasswordField();
|
||||
Platform.runLater(() -> pinField.requestFocus());
|
||||
enterPinButton = new Button("Enter PIN");
|
||||
enterPinButton.setDefaultButton(true);
|
||||
enterPinButton.setOnAction(event -> {
|
||||
enterPinButton.setDisable(true);
|
||||
sendPin(pinField.getText());
|
||||
@@ -316,16 +371,32 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private Node getPassphraseEntry() {
|
||||
CustomTextField passphraseField = (CustomTextField)TextFields.createClearableTextField();
|
||||
CustomPasswordField passphraseField = new ViewPasswordField();
|
||||
passphrase.bind(passphraseField.textProperty());
|
||||
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||
|
||||
Button sendPassphraseButton = new Button("Send Passphrase");
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
passphraseField.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
sendPassphrase(passphrase.get());
|
||||
});
|
||||
|
||||
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
|
||||
sendPassphraseButton.setText("Send Passphrase");
|
||||
sendPassphraseButton.getStyleClass().add("default-button");
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
sendPassphrase(passphrase.get());
|
||||
});
|
||||
|
||||
MenuItem removePassphrase = new MenuItem("Toggle Passphrase Off");
|
||||
removePassphrase.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Toggling passphrase off, check device...");
|
||||
togglePassphraseOff();
|
||||
});
|
||||
sendPassphraseButton.getItems().add(removePassphrase);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
@@ -338,6 +409,35 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getTogglePassphraseOn() {
|
||||
CopyableLabel label = new CopyableLabel("Passphrase is currently disabled");
|
||||
HBox.setHgrow(label, Priority.ALWAYS);
|
||||
|
||||
Button togglePassphraseOn = new Button("Toggle Passphrase On");
|
||||
togglePassphraseOn.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
hideButtons(importButton, signButton, displayAddressButton, signMessageButton);
|
||||
setDescription("Toggling passphrase on, check device...");
|
||||
togglePassphraseOn();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.setAlignment(Pos.CENTER_LEFT);
|
||||
contentBox.getChildren().addAll(label, togglePassphraseOn);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private void hideButtons(Node... buttons) {
|
||||
for(Node button : buttons) {
|
||||
if(button != null) {
|
||||
button.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void promptPin() {
|
||||
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
|
||||
promptPinService.setOnSucceeded(workerStateEvent -> {
|
||||
@@ -351,7 +451,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
promptPinService.setOnFailed(workerStateEvent -> {
|
||||
setError(promptPinService.getException().getMessage(), null);
|
||||
setError("Error", promptPinService.getException().getMessage());
|
||||
unlockButton.setDisable(false);
|
||||
});
|
||||
promptPinService.start();
|
||||
@@ -367,13 +467,16 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
setExpanded(false);
|
||||
unlockButton.setVisible(false);
|
||||
|
||||
if(device.getNeedsPassphraseSent()) {
|
||||
if(device.isNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
setPassphraseButton.setDisable(true);
|
||||
setContent(getPassphraseEntry());
|
||||
setExpanded(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
setContent(getTogglePassphraseOn());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError("Incorrect PIN", null);
|
||||
@@ -384,7 +487,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
sendPinService.setOnFailed(workerStateEvent -> {
|
||||
setError(sendPinService.getException().getMessage(), null);
|
||||
setError("Error", sendPinService.getException().getMessage());
|
||||
enterPinButton.setDisable(false);
|
||||
});
|
||||
setDescription("Unlocking...");
|
||||
@@ -413,13 +516,43 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError(enumerateService.getException().getMessage(), null);
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
setPassphraseButton.setDisable(false);
|
||||
setPassphraseButton.setVisible(true);
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
|
||||
private void togglePassphraseOff() {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
|
||||
device.setNeedsPassphraseSent(false);
|
||||
setPassphraseButton.setVisible(false);
|
||||
setDescription("Unlocked");
|
||||
showOperationButton();
|
||||
});
|
||||
togglePassphraseService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", togglePassphraseService.getException().getMessage());
|
||||
});
|
||||
togglePassphraseService.start();
|
||||
}
|
||||
|
||||
private void togglePassphraseOn() {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
|
||||
device.setNeedsPassphraseSent(true);
|
||||
setPassphraseButton.setVisible(true);
|
||||
setPassphraseButton.setDisable(true);
|
||||
setDescription("Enter passphrase");
|
||||
setContent(getPassphraseEntry());
|
||||
setExpanded(true);
|
||||
});
|
||||
togglePassphraseService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", togglePassphraseService.getException().getMessage());
|
||||
});
|
||||
togglePassphraseService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
if(device.getFingerprint() == null) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||
@@ -434,7 +567,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importXpub(derivation);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError(enumerateService.getException().getMessage(), null);
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
enumerateService.start();
|
||||
@@ -450,14 +583,29 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.setOnSucceeded(workerStateEvent -> {
|
||||
String xpub = getXpubService.getValue();
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(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));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
});
|
||||
getXpubService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubService.getException().getMessage());
|
||||
@@ -476,7 +624,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
@@ -485,7 +633,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath());
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||
displayAddressService.setOnSucceeded(successEvent -> {
|
||||
String address = displayAddressService.getValue();
|
||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||
@@ -512,21 +660,89 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signMessageService.start();
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
|
||||
return;
|
||||
}
|
||||
|
||||
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||
|
||||
Wallet copyWallet = wallet.copy();
|
||||
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
for(StandardAccount availableAccount : availableAccounts) {
|
||||
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
|
||||
Keystore availableKeystore = availableWallet.getKeystores().get(0);
|
||||
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
|
||||
accountDerivationPaths.put(availableAccount, derivationPath);
|
||||
}
|
||||
|
||||
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
|
||||
getXpubsService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||
importedKeystores.put(entry.getKey(), keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores);
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
importedKeystores.keySet().retainAll(accountDiscoveryService.getValue());
|
||||
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
||||
});
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
log.error("Failed to discover accounts", event.getSource().getException());
|
||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
accountDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
setDescription("Discovering...");
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setDefaultButton(defaultDevice);
|
||||
signButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
|
||||
displayAddressButton.setDefaultButton(defaultDevice);
|
||||
displayAddressButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
|
||||
signMessageButton.setDefaultButton(defaultDevice);
|
||||
signMessageButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
|
||||
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||
discoverKeystoresButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,16 +750,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(keyDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importDerivationButton = new Button("Import");
|
||||
Button importDerivationButton = new Button("Import Custom Derivation");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
@@ -552,7 +770,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
@@ -566,7 +785,11 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public Device getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(psbt, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(psbt, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -26,8 +26,8 @@ public class DeviceSignMessageDialog extends DeviceDialog<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, message, keyDerivation, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(wallet, message, keyDerivation, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -16,16 +20,24 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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;
|
||||
|
||||
class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
||||
|
||||
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 EntryCell() {
|
||||
super();
|
||||
@@ -59,14 +71,20 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
tooltip.setOnShowing(event -> {
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
});
|
||||
}
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
|
||||
});
|
||||
@@ -75,65 +93,77 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||
increaseFeeGlyph.setFontSize(12);
|
||||
increaseFeeButton.setGraphic(increaseFeeGlyph);
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFeeButton.setOnAction(event -> {
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
actionBox.getChildren().add(increaseFeeButton);
|
||||
}
|
||||
|
||||
if(blockTransaction.getHeight() <= 0 && containsWalletOutputs(transactionEntry)) {
|
||||
Button cpfpButton = new Button("");
|
||||
cpfpButton.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
cpfpButton.setOnAction(event -> {
|
||||
createCpfp(transactionEntry);
|
||||
});
|
||||
actionBox.getChildren().add(cpfpButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button receiveButton = new Button("");
|
||||
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
|
||||
receiveGlyph.setFontSize(12);
|
||||
receiveButton.setGraphic(receiveGlyph);
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
|
||||
(nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
||||
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("");
|
||||
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||
signMessageGlyph.setFontSize(12);
|
||||
signMessageButton.setGraphic(signMessageGlyph);
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
actionBox.getChildren().add(signMessageButton);
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
|
||||
if(nodeEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
setText(address.toString().substring(0, 20) + "...");
|
||||
setContextMenu(null);
|
||||
setGraphic(new HBox());
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||
setTooltip(tooltip);
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
|
||||
});
|
||||
@@ -141,16 +171,14 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
Button spendUtxoButton = new Button("");
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.setFontSize(12);
|
||||
spendUtxoButton.setGraphic(sendGlyph);
|
||||
spendUtxoButton.setGraphic(getSendGlyph());
|
||||
spendUtxoButton.setOnAction(event -> {
|
||||
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
|
||||
});
|
||||
actionBox.getChildren().add(spendUtxoButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +191,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.filter(TransactionInput::isReplaceByFeeEnabled)
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -173,9 +202,16 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
|
||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
int 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);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
Collections.shuffle(walletUtxos);
|
||||
@@ -189,26 +225,85 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||
externalOutputs.removeAll(ourOutputs);
|
||||
externalOutputs.addAll(consolidationOutputs);
|
||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||
try {
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(label);
|
||||
if(matcher.matches()) {
|
||||
String base = matcher.group(1);
|
||||
if(matcher.groupCount() > 2 && matcher.group(3) != null) {
|
||||
int count = Integer.parseInt(matcher.group(3)) + 1;
|
||||
label = base + "(Replaced By Fee #" + count + ")";
|
||||
} else {
|
||||
label = base + "(Replaced By Fee #2)";
|
||||
}
|
||||
} else {
|
||||
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
|
||||
}
|
||||
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating RBF payment", e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(payments.isEmpty()) {
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
if(AppServices.getTargetBlockFeeRates().isEmpty()) {
|
||||
if(AppServices.getTargetBlockFeeRates() == null || AppServices.getTargetBlockFeeRates().isEmpty()) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
return AppServices.getTargetBlockFeeRates().values().iterator().next();
|
||||
}
|
||||
|
||||
private static void createCpfp(TransactionEntry transactionEntry) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(HashIndexEntry::getHashIndex)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(ourOutputs.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
|
||||
}
|
||||
|
||||
BlockTransactionHashIndex utxo = ourOutputs.get(0);
|
||||
|
||||
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
|
||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
return transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.anyMatch(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT));
|
||||
}
|
||||
|
||||
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
@@ -226,19 +321,141 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() != Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(null);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() == Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(null));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private String getTooltip(TransactionEntry transactionEntry) {
|
||||
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
if(!AppServices.getMempoolHistogram().isEmpty()) {
|
||||
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
|
||||
double vSize = transactionEntry.getBlockTransaction().getTransaction().getVirtualSize();
|
||||
double feeRate = transactionEntry.getBlockTransaction().getFee() / vSize;
|
||||
long vSizefromTip = rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
|
||||
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 += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB (" + amount + " from tip)";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private static Glyph getViewTransactionGlyph() {
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
return searchGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeRBFGlyph() {
|
||||
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||
increaseFeeGlyph.setFontSize(12);
|
||||
return increaseFeeGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeCPFPGlyph() {
|
||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||
cpfpGlyph.setFontSize(12);
|
||||
return cpfpGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getReceiveGlyph() {
|
||||
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
|
||||
receiveGlyph.setFontSize(12);
|
||||
return receiveGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getSignMessageGlyph() {
|
||||
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||
signMessageGlyph.setFontSize(12);
|
||||
return signMessageGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getSendGlyph() {
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.setFontSize(12);
|
||||
return sendGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getCopyGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COPY);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getFreezeGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SNOWFLAKE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getUnfreezeGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SUN);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(containsWalletOutputs(transactionEntry)) {
|
||||
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
createCpfp.setOnAction(AE -> {
|
||||
hide();
|
||||
createCpfp(transactionEntry);
|
||||
});
|
||||
|
||||
getItems().add(createCpfp);
|
||||
}
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -248,21 +465,18 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
|
||||
getItems().add(copyTxid);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee");
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -287,12 +501,34 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copyDate, copyTxid, copyHeight);
|
||||
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
hide();
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -318,34 +554,22 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
|
||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
hide();
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HashIndexEntryContextMenu extends ContextMenu {
|
||||
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
|
||||
MenuItem copyHashIndex = new MenuItem(label);
|
||||
copyHashIndex.setOnAction(AE -> {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hashIndexEntry.getHashIndex().toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), hashIndexEntry.getBlockTransaction()));
|
||||
});
|
||||
getItems().add(copyHashIndex);
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
MenuItem sendSelected = new MenuItem("Send Selected");
|
||||
sendSelected.setGraphic(getSendGlyph());
|
||||
sendSelected.setOnAction(AE -> {
|
||||
hide();
|
||||
sendSelectedUtxos(treeTableView, hashIndexEntry);
|
||||
@@ -356,20 +580,32 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
if(hashIndexEntry.getHashIndex().getStatus() == null || hashIndexEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||
MenuItem freezeUtxo = new MenuItem("Freeze UTXO");
|
||||
freezeUtxo.setGraphic(getFreezeGlyph());
|
||||
freezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
freezeUtxo(hashIndexEntry);
|
||||
freezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(freezeUtxo);
|
||||
} else {
|
||||
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
|
||||
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
unfreezeUtxo(hashIndexEntry);
|
||||
unfreezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(unfreezeUtxo);
|
||||
}
|
||||
}
|
||||
|
||||
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
|
||||
MenuItem copyHashIndex = new MenuItem(label);
|
||||
copyHashIndex.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hashIndexEntry.getHashIndex().toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyHashIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +616,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
cell.getStyleClass().remove("address-cell");
|
||||
cell.getStyleClass().remove("hashindex-row");
|
||||
cell.getStyleClass().remove("confirming");
|
||||
cell.getStyleClass().remove("negative-amount");
|
||||
cell.getStyleClass().remove("spent");
|
||||
cell.getStyleClass().remove("unspendable");
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.FileImport;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
@@ -40,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected ButtonBase importButton;
|
||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||
private final boolean scannable;
|
||||
private final boolean fileFormatAvailable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, imageUrl);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
this.fileFormatAvailable = fileFormatAvailable;
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -53,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && fileFormatAvailable) {
|
||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -74,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
importButton = new Button("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
importButton.setGraphic(cameraGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importQR();
|
||||
});
|
||||
return importButton;
|
||||
} else {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -95,6 +109,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
importFile(file, null);
|
||||
@@ -111,8 +126,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
importButton.setDisable(true);
|
||||
setExpanded(true);
|
||||
} else {
|
||||
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
importFile(file.getName(), inputStream, password);
|
||||
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
importFile(file.getName(), inputStream, password);
|
||||
};
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error importing file", e);
|
||||
@@ -142,6 +158,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.outputDescriptor != null) {
|
||||
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
@@ -161,6 +186,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error importing QR", result.exception);
|
||||
setError("Import Error", result.exception.getMessage());
|
||||
} else {
|
||||
setError("Import Error", null);
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,11 +197,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
if(wallets != null) {
|
||||
for(Wallet wallet : wallets) {
|
||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
return wallet.getKeystores().get(0);
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
return keystore;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImportException("Script type " + scriptType + " is not supported");
|
||||
throw new ImportException("Script type " + scriptType.getDescription() + " is not supported in this QR. Check you are displaying the correct QR code.");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -182,7 +214,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
||||
|
||||
private Node getPasswordEntry(File file) {
|
||||
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
|
||||
CustomPasswordField passwordField = new ViewPasswordField();
|
||||
passwordField.setPromptText("Wallet password");
|
||||
password.bind(passwordField.textProperty());
|
||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -12,11 +13,13 @@ import java.io.*;
|
||||
public class FileKeystoreImportPane extends FileImportPane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreFileImport importer;
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
}
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
@@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
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() + ".");
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,50 +2,91 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.WalletExport;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
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.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final WalletExport exporter;
|
||||
private final boolean scannable;
|
||||
|
||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isWalletExportScannable();
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
Button exportButton = new Button("Export Wallet...");
|
||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
exportButton.setOnAction(event -> {
|
||||
exportWallet();
|
||||
});
|
||||
return exportButton;
|
||||
if(scannable) {
|
||||
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 {
|
||||
Button exportButton = new Button("Export File...");
|
||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
exportButton.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
return exportButton;
|
||||
}
|
||||
}
|
||||
|
||||
private void exportWallet() {
|
||||
private void exportQR() {
|
||||
exportWallet(null);
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
fileName = wallet.getMasterName();
|
||||
}
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
exportWallet(file);
|
||||
@@ -53,43 +94,61 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void exportWallet(File file) {
|
||||
Wallet copy = wallet.copy();
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
exportWallet(file, decryptedWallet);
|
||||
|
||||
try {
|
||||
exportWallet(file, decryptedWallet);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
setError("Export Error", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
exportWallet(file, copy);
|
||||
exportWallet(file, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportWallet(File file, Wallet decryptedWallet) {
|
||||
private void exportWallet(File file, Wallet exportWallet) {
|
||||
try {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
exporter.exportWallet(decryptedWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(decryptedWallet));
|
||||
if(file != null) {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
}
|
||||
} else {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
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);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
private final WalletImport importer;
|
||||
|
||||
public FileWalletImportPane(WalletImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
@@ -21,12 +22,15 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
|
||||
@@ -36,41 +40,87 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private byte[] fileBytes;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
try {
|
||||
fileBytes = ByteStreams.toByteArray(inputStream);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Could not read file", e);
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
|
||||
Wallet wallet = wallets.get(0);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallet.setName(importer.getName());
|
||||
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
|
||||
} else {
|
||||
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
|
||||
if(scriptTypes.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileBytes = ByteStreams.toByteArray(inputStream);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Could not read file", e);
|
||||
}
|
||||
}
|
||||
|
||||
setContent(getScriptTypeEntry());
|
||||
setContent(getScriptTypeEntry(scriptTypes));
|
||||
setExpanded(true);
|
||||
importButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void importWallet(ScriptType scriptType) throws ImportException {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(fileName);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
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));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||
Label label = new Label("Script Type:");
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
@@ -97,7 +147,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().addAll(label, scriptTypeComboBox, region, importFileButton);
|
||||
contentBox.getChildren().addAll(label, fieldBox, region, importFileButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class HelpLabel extends Label {
|
||||
@@ -14,15 +19,18 @@ public class HelpLabel extends Label {
|
||||
super("", getHelpGlyph());
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(helpTextProperty());
|
||||
setTooltip(tooltip);
|
||||
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
getStyleClass().add("help-label");
|
||||
|
||||
Platform.runLater(() -> setTooltip(tooltip));
|
||||
}
|
||||
|
||||
private static Glyph getHelpGlyph() {
|
||||
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
lockGlyph.getStyleClass().add("help-icon");
|
||||
lockGlyph.setFontSize(12);
|
||||
return lockGlyph;
|
||||
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
glyph.getStyleClass().add("help-icon");
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public final StringProperty helpTextProperty() {
|
||||
@@ -42,4 +50,18 @@ public class HelpLabel extends Label {
|
||||
public final String getHelpText() {
|
||||
return helpText == null ? "" : helpText.getValue();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> helpGraphicProperty() {
|
||||
if(helpGraphicProperty == null) {
|
||||
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
|
||||
}
|
||||
|
||||
return helpGraphicProperty;
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> helpGraphicProperty;
|
||||
|
||||
public final void setHelpGraphic(Node graphic) {
|
||||
helpGraphicProperty().setValue(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import javafx.application.Platform;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
@@ -20,16 +21,21 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
}
|
||||
|
||||
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
|
||||
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
|
||||
this(walletName, keystore, false);
|
||||
}
|
||||
|
||||
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
|
||||
this.passphrase = new ViewPasswordField();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
|
||||
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
|
||||
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
|
||||
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.KEY);
|
||||
key.setFontSize(50);
|
||||
@@ -39,6 +45,13 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warnGlyph.getStyleClass().add("warn-icon");
|
||||
warnGlyph.setFontSize(12);
|
||||
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
|
||||
warnLabel.setGraphicTextGap(5);
|
||||
content.getChildren().add(warnLabel);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
Platform.runLater(passphrase::requestFocus);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.util.converter.DefaultStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -28,10 +29,11 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
|
||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
|
||||
setText(label);
|
||||
setContextMenu(new LabelContextMenu(label));
|
||||
setContextMenu(new LabelContextMenu(entry, label));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(String label) {
|
||||
public LabelContextMenu(Entry entry, String label) {
|
||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||
copyLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -88,8 +90,17 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
content.putString(label);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copyLabel);
|
||||
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
public class MasterKeyDisplayDialog extends Dialog<Void> {
|
||||
public MasterKeyDisplayDialog(Keystore decryptedKeystore) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
AnchorPane anchorPane = new AnchorPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.getStyleClass().add("edge-to-edge");
|
||||
scrollPane.setPrefHeight(200);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
XprvKeystoreImportPane keystorePane = new XprvKeystoreImportPane(decryptedKeystore);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(260);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
categoryAxis.setTickLabelRotation(0);
|
||||
|
||||
NumberAxis numberAxis = (NumberAxis)getYAxis();
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
this.setOnMouseMoved(mouseEvent -> {
|
||||
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
|
||||
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
|
||||
@@ -91,9 +79,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
series.setName(feeRate + "+ sats/vB");
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
previousFeeRate = feeRate;
|
||||
}
|
||||
|
||||
final double maxMvB = getMaxMvB(getData());
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
if(maxMvB > Y_VALUE_BREAK_MVB) {
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
} else {
|
||||
return (vSizeBytes / (1000)) + " kvB";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if(categories.keySet().iterator().hasNext()) {
|
||||
String time = categories.values().iterator().next();
|
||||
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
|
||||
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
|
||||
double maxTotal = 0d;
|
||||
for(Series<String, Number> series : seriesList) {
|
||||
maxTotal = Math.max(maxTotal, getMaxMvB(series));
|
||||
}
|
||||
|
||||
return maxTotal;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(Series<String, Number> series) {
|
||||
double total = 0d;
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
total += mvb;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static class ChartTooltip extends VBox {
|
||||
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
|
||||
Label title = new Label("At " + time);
|
||||
HBox titleBox = new HBox(title);
|
||||
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
|
||||
getChildren().add(titleBox);
|
||||
double maxMvB = getMaxMvB(seriesList);
|
||||
|
||||
for(int i = seriesList.size() - 1; i >= 0; i--) {
|
||||
Series<String, Number> series = seriesList.get(i);
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
if(data.getXValue().equals(category)) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
if(mvb >= 0.01) {
|
||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||
double kvb = data.getYValue().doubleValue() / 1000;
|
||||
double mvb = kvb / 1000;
|
||||
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");
|
||||
Label label = new Label(series.getName() + ": " + amount);
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
@@ -34,6 +35,7 @@ import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -43,8 +45,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
private final ToggleGroup formatGroup;
|
||||
private final ToggleButton formatTrezor;
|
||||
private final ToggleButton formatElectrum;
|
||||
private final Wallet wallet;
|
||||
private WalletNode walletNode;
|
||||
private boolean electrumSignatureFormat;
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* Verification only constructor
|
||||
@@ -69,13 +76,25 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
* @param walletNode Wallet node to derive address from
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
|
||||
this(wallet, walletNode, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and verify with preset address, and supplied title, message and dialog buttons
|
||||
*
|
||||
* @param wallet Wallet to sign with
|
||||
* @param walletNode Wallet node to derive address from
|
||||
* @param title Header text of dialog
|
||||
* @param msg Message to sign (all fields will be made uneditable)
|
||||
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||
if(walletNode != null) {
|
||||
checkWalletSigning(walletNode.getWallet());
|
||||
}
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
|
||||
}
|
||||
checkWalletSigning(wallet);
|
||||
}
|
||||
|
||||
this.wallet = wallet;
|
||||
@@ -83,8 +102,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
|
||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if (!image.isError()) {
|
||||
@@ -100,6 +120,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
@@ -109,7 +130,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
address.setText(wallet.getAddress(walletNode).toString());
|
||||
address.setText(walletNode.getAddress().toString());
|
||||
}
|
||||
|
||||
Field messageField = new Field();
|
||||
@@ -129,51 +150,90 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
signature.setWrapText(true);
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
fieldset.getChildren().addAll(addressField, messageField, signatureField);
|
||||
Field formatField = new Field();
|
||||
formatField.setText("Format:");
|
||||
formatGroup = new ToggleGroup();
|
||||
formatElectrum = new ToggleButton("Standard (Electrum)");
|
||||
formatTrezor = new ToggleButton("BIP137 (Trezor)");
|
||||
SegmentedButton formatButtons = new SegmentedButton(formatElectrum, formatTrezor);
|
||||
formatButtons.setToggleGroup(formatGroup);
|
||||
formatField.getInputs().add(formatButtons);
|
||||
|
||||
formatGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
electrumSignatureFormat = (newValue == formatElectrum);
|
||||
});
|
||||
|
||||
formatButtons.setDisable(wallet != null && walletNode != null && wallet.getScriptType() == ScriptType.P2PKH);
|
||||
|
||||
fieldset.getChildren().addAll(addressField, messageField, signatureField, formatField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
if(msg != null) {
|
||||
message.setText(msg);
|
||||
address.setEditable(false);
|
||||
message.setEditable(false);
|
||||
signature.setEditable(false);
|
||||
formatButtons.setDisable(true);
|
||||
}
|
||||
|
||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(wallet == null);
|
||||
signButton.setOnAction(event -> {
|
||||
signMessage();
|
||||
});
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
|
||||
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
|
||||
verifyButton.setDefaultButton(false);
|
||||
verifyButton.setOnAction(event -> {
|
||||
verifyMessage();
|
||||
});
|
||||
|
||||
boolean validAddress = isValidAddress();
|
||||
signButton.setDisable(!validAddress || (wallet == null));
|
||||
verifyButton.setDisable(!validAddress);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = isValidAddress();
|
||||
signButton.setDisable(!valid || (wallet == null));
|
||||
verifyButton.setDisable(!valid);
|
||||
|
||||
if(valid && wallet != null) {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
|
||||
if(customSignButtonType != null) {
|
||||
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
|
||||
customSignButton.setDefaultButton(true);
|
||||
customSignButton.setOnAction(event -> {
|
||||
customSignButton.setDisable(true);
|
||||
signMessage();
|
||||
setResult(ButtonBar.ButtonData.OK_DONE);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(wallet == null);
|
||||
signButton.setOnAction(event -> {
|
||||
signMessage();
|
||||
});
|
||||
|
||||
Button verifyButton = (Button) dialogPane.lookupButton(verifyButtonType);
|
||||
verifyButton.setDefaultButton(false);
|
||||
verifyButton.setOnAction(event -> {
|
||||
verifyMessage();
|
||||
});
|
||||
|
||||
boolean validAddress = isValidAddress();
|
||||
signButton.setDisable(!validAddress || (wallet == null));
|
||||
verifyButton.setDisable(!validAddress);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = isValidAddress();
|
||||
signButton.setDisable(!valid || (wallet == null));
|
||||
verifyButton.setDisable(!valid);
|
||||
|
||||
if(valid && wallet != null) {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -182,20 +242,58 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().unregister(this);
|
||||
if(!closed) {
|
||||
EventManager.get().unregister(this);
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
address.requestFocus();
|
||||
} else if(message.getText().isEmpty()) {
|
||||
message.requestFocus();
|
||||
}
|
||||
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
});
|
||||
}
|
||||
|
||||
private void checkWalletSigning(Wallet wallet) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
|
||||
}
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the Electrum signing format, which uses the non-segwit compressed signing parameters for both segwit types (p2sh-p2wpkh and p2wpkh)
|
||||
*
|
||||
* @param electrumSignatureFormat
|
||||
*/
|
||||
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
|
||||
formatGroup.selectToggle(electrumSignatureFormat ? formatElectrum : formatTrezor);
|
||||
this.electrumSignatureFormat = electrumSignatureFormat;
|
||||
}
|
||||
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
return true;
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -211,25 +309,28 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(wallet.containsSeeds()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
signUnencryptedKeystore(wallet);
|
||||
signUnencryptedKeystore(signingWallet);
|
||||
}
|
||||
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(wallet);
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(signingWallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
//Note we can expect a single keystore due to the check above
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
|
||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
} catch(Exception e) {
|
||||
log.error("Could not sign message", e);
|
||||
AppServices.showErrorDialog("Could not sign message", e.getMessage());
|
||||
@@ -274,6 +375,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
alert.setTitle("Verification Succeeded");
|
||||
alert.setHeaderText("Verification Succeeded");
|
||||
alert.setContentText("The signature verified against the message.");
|
||||
AppServices.moveToActiveWindowScreen(alert);
|
||||
alert.showAndWait();
|
||||
} else {
|
||||
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
|
||||
@@ -308,20 +410,21 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(walletNode.getWallet().copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
signUnencryptedKeystore(decryptedWallet);
|
||||
decryptedWallet.clearPrivate();
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
setGraphicTextGap(8);
|
||||
getStyleClass().add("mixstatus-cell");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
|
||||
super.updateItem(mixStatus, empty);
|
||||
|
||||
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
|
||||
|
||||
if(empty || mixStatus == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason != MixFailReason.CANCEL) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||
tt.setOnShowing(event -> {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
|
||||
registeredInputsService.setOnSucceeded(eventStateHandler -> {
|
||||
if(registeredInputsService.getValue() != null) {
|
||||
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
|
||||
}
|
||||
});
|
||||
registeredInputsService.start();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.TilePane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
private void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size()));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
}
|
||||
@@ -3,59 +3,48 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicImport importer;
|
||||
|
||||
private SplitMenuButton enterMnemonicButton;
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private TilePane wordsPane;
|
||||
private Button generateButton;
|
||||
private Label validLabel;
|
||||
private Label invalidLabel;
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private Button confirmButton;
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
private SimpleListProperty<String> wordEntriesProperty;
|
||||
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
|
||||
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
|
||||
@@ -63,41 +52,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
public MnemonicKeystoreImportPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
this.wallet = null;
|
||||
this.importer = null;
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Set Words Size");
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
enterMnemonic(24);
|
||||
});
|
||||
int[] numberWords = new int[] {24, 21, 18, 15, 12};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem(numberWords[i] + " words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -107,7 +61,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
@@ -123,148 +77,95 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void enterMnemonic(int numWords) {
|
||||
protected void enterMnemonic(int numWords) {
|
||||
generatedMnemonicCode = null;
|
||||
setDescription("Enter mnemonic word list");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords, false));
|
||||
setExpanded(true);
|
||||
super.enterMnemonic(numWords);
|
||||
}
|
||||
|
||||
private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
protected List<Node> createLeftButtons() {
|
||||
generateButton = new Button("Generate New");
|
||||
generateButton.setOnAction(event -> {
|
||||
generateNew();
|
||||
});
|
||||
generateButton.managedProperty().bind(generateButton.visibleProperty());
|
||||
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
return List.of(generateButton);
|
||||
}
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
protected List<Node> createRightButtons() {
|
||||
confirmButton = new Button("Re-enter Words...");
|
||||
confirmButton.setOnAction(event -> {
|
||||
confirmBackup();
|
||||
});
|
||||
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
|
||||
confirmButton.setVisible(false);
|
||||
confirmButton.setDefaultButton(true);
|
||||
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
WordEntry wordEntry = new WordEntry(i, wordEntryList);
|
||||
wordsPane.getChildren().add(wordEntry);
|
||||
}
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
displayMnemonicCode();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(!displayWordsOnly) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
nextButton = new Button("Confirm Backup...");
|
||||
nextButton.setOnAction(event -> {
|
||||
confirmRecord();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
|
||||
nextButton.setVisible(false);
|
||||
nextButton.setDefaultButton(true);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
buttonPane.setPadding(new Insets(0, 26, 0, 10));
|
||||
return List.of(backButton, nextButton, confirmButton, calculateButton);
|
||||
}
|
||||
|
||||
generateButton = new Button("Generate New");
|
||||
generateButton.setOnAction(event -> {
|
||||
generateNew();
|
||||
});
|
||||
generateButton.managedProperty().bind(generateButton.visibleProperty());
|
||||
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
|
||||
buttonPane.getChildren().add(generateButton);
|
||||
AnchorPane.setLeftAnchor(generateButton, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
validLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(validLabel);
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
invalidLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(invalidLabel);
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
confirmButton = new Button("Re-enter Words...");
|
||||
confirmButton.setOnAction(event -> {
|
||||
confirmBackup();
|
||||
});
|
||||
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
|
||||
confirmButton.setVisible(false);
|
||||
confirmButton.setDefaultButton(true);
|
||||
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
|
||||
buttonPane.getChildren().add(confirmButton);
|
||||
AnchorPane.setRightAnchor(confirmButton, 0.0);
|
||||
|
||||
calculateButton = new Button("Calculate Seed");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Calculate the seed from the provided word list"));
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
displayMnemonicCode();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
|
||||
backButton.setVisible(false);
|
||||
|
||||
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
|
||||
boolean empty = true;
|
||||
boolean validWords = true;
|
||||
boolean validChecksum = false;
|
||||
for(String word : wordEntryList) {
|
||||
if(!word.isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
generateButton.setVisible(empty && generatedMnemonicCode == null);
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
rightBox.getChildren().addAll(backButton, calculateButton);
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
}
|
||||
}
|
||||
|
||||
return vBox;
|
||||
generateButton.setVisible(empty && generatedMnemonicCode == null);
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private void generateNew() {
|
||||
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
|
||||
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
|
||||
SecureRandom secureRandom;
|
||||
try {
|
||||
secureRandom = SecureRandom.getInstanceStrong();
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
|
||||
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
|
||||
|
||||
displayMnemonicCode();
|
||||
@@ -275,7 +176,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
calculateButton.setVisible(false);
|
||||
confirmButton.setVisible(true);
|
||||
nextButton.setVisible(true);
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
|
||||
@@ -287,12 +188,42 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
|
||||
StackPane wordsStackPane = (StackPane)getContent();
|
||||
if(wordsStackPane.getChildren().size() > 1) {
|
||||
wordsStackPane.getChildren().remove(1);
|
||||
confirmButton.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmRecord() {
|
||||
setDescription("Confirm words have been recorded");
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
StackPane wordsPane = (StackPane)getContent();
|
||||
StackPane confirmPane = new StackPane();
|
||||
confirmPane.setMaxWidth(350);
|
||||
confirmPane.setMaxHeight(100);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(confirmPane.getMaxWidth());
|
||||
region.setMinHeight(confirmPane.getMaxHeight());
|
||||
confirmPane.getStyleClass().add("box-overlay");
|
||||
Node wrappedRegion = Borders.wrap(region).lineBorder().innerPadding(0).outerPadding(0).buildAll();
|
||||
Label label = new Label("Have these " + wordEntriesProperty.get().size() + " words been written down?\nIn the next step, you will need to re-enter them.");
|
||||
confirmPane.getChildren().addAll(wrappedRegion, label);
|
||||
wordsPane.getChildren().add(confirmPane);
|
||||
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
nextButton.setVisible(false);
|
||||
confirmButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void confirmBackup() {
|
||||
setDescription("Confirm backup by re-entering words");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
@@ -311,7 +242,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
@@ -321,6 +252,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
@@ -331,99 +270,12 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setSpacing(10);
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField();
|
||||
wordField.setMaxWidth(100);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
Validator.createEmptyValidator("Word is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
wordEntryList.set(wordNumber, newValue);
|
||||
});
|
||||
|
||||
this.getChildren().addAll(label, wordField);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
private static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList) {
|
||||
this.wordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
if(word.equals(request.getUserText())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
private class PassphraseEntry extends HBox {
|
||||
public PassphraseEntry() {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
CustomTextField passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setStyle("-fx-padding: 0 0 0 0");
|
||||
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
@@ -431,13 +283,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importDerivationButton = new Button("Import");
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
@@ -446,7 +299,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
@@ -459,30 +313,4 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size(), true));
|
||||
setExpanded(true);
|
||||
|
||||
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("valid-checksum");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("invalid-checksum");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
protected SplitMenuButton enterMnemonicButton;
|
||||
protected TilePane wordsPane;
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
protected SimpleListProperty<String> wordEntriesProperty;
|
||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
|
||||
protected IntegerProperty defaultWordSizeProperty;
|
||||
|
||||
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||
super(title, description, content, imageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 24 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(24);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {24, 21, 18, 15, 12};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected void enterMnemonic(int numWords) {
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
buttonPane.setPadding(new Insets(0, 26, 0, 10));
|
||||
|
||||
HBox leftBox = new HBox(10);
|
||||
leftBox.getChildren().addAll(createLeftButtons());
|
||||
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
validLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(validLabel);
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
invalidLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(invalidLabel);
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
|
||||
boolean empty = true;
|
||||
boolean validWords = true;
|
||||
boolean validChecksum = false;
|
||||
for(String word : wordEntryList) {
|
||||
if(!word.isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
|
||||
onWordChange(empty, validWords, validChecksum);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
rightBox.getChildren().addAll(createRightButtons());
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
|
||||
Platform.runLater(() -> wordEntries.get(0).getEditor().requestFocus());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
protected List<Node> createLeftButtons() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setSpacing(10);
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField() {
|
||||
@Override
|
||||
public void paste() {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
|
||||
String[] words = clipboard.getString().split("[\\n\\s]");
|
||||
WordEntry entry = WordEntry.this;
|
||||
for(String word : words) {
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
entry = entry.nextEntry;
|
||||
if(entry == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.paste();
|
||||
}
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
if(!text.isEmpty()) {
|
||||
String newText = text.replace(" ", "").toLowerCase();
|
||||
int carretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
// fix caret position based on difference in originally added text and fixed text
|
||||
change.selectRange(carretPos, carretPos);
|
||||
}
|
||||
return change;
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
Validator.createEmptyValidator("Word is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
wordEntryList.set(wordNumber, newValue);
|
||||
});
|
||||
|
||||
this.getChildren().addAll(label, wordField);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList) {
|
||||
this.wordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
protected class PassphraseEntry extends HBox {
|
||||
private final CustomTextField passphraseField;
|
||||
|
||||
public PassphraseEntry() {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setStyle("-fx-padding: 0 0 0 0");
|
||||
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return passphraseField;
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicWalletKeystoreImportPane.class);
|
||||
|
||||
private final KeystoreMnemonicImport importer;
|
||||
|
||||
private Button discoverButton;
|
||||
private Button importButton;
|
||||
|
||||
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter word list");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Node> createRightButtons() {
|
||||
discoverButton = new Button("Discover Wallet");
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setDefaultButton(true);
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverWallet();
|
||||
});
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setTooltip(new Tooltip("Look for existing transactions from the provided word list"));
|
||||
discoverButton.visibleProperty().bind(AppServices.onlineProperty());
|
||||
|
||||
importButton = new Button("Import Wallet");
|
||||
importButton.setDisable(true);
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||
importButton.setOnAction(event -> {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
});
|
||||
|
||||
return List.of(discoverButton, importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discoverButton.setDisable(!validChecksum || !AppServices.isConnected());
|
||||
importButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private void discoverWallet() {
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setMaxHeight(discoverButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
discoverButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
derivations.add(List.of(new ChildNumber(0, true)));
|
||||
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
|
||||
for(List<ChildNumber> derivation : derivations) {
|
||||
try {
|
||||
Wallet wallet = getWallet(scriptType, derivation);
|
||||
wallets.add(wallet);
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
discoverButton.setDisable(!AppServices.isConnected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
|
||||
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
|
||||
walletDiscoveryService.setOnSucceeded(successEvent -> {
|
||||
discoverButton.setGraphic(null);
|
||||
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
|
||||
if(optWallet.isPresent()) {
|
||||
EventManager.get().post(new WalletImportEvent(optWallet.get()));
|
||||
} else {
|
||||
discoverButton.setDisable(false);
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(failedEvent -> {
|
||||
discoverButton.setGraphic(null);
|
||||
log.error("Failed to discover wallets", failedEvent.getSource().getException());
|
||||
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
}
|
||||
|
||||
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
Wallet wallet = new Wallet("");
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
Label label = new Label("Script Type:");
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
|
||||
Button importMnemonicButton = new Button("Import");
|
||||
importMnemonicButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
ScriptType scriptType = scriptTypeComboBox.getValue();
|
||||
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing mnemonic", e);
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().addAll(label, fieldBox, region, importMnemonicButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PayNymAvatar extends StackPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
|
||||
|
||||
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
|
||||
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public PayNymAvatar() {
|
||||
super();
|
||||
|
||||
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
|
||||
if(paymentCode == null) {
|
||||
getChildren().clear();
|
||||
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
|
||||
String cacheId = getCacheId(paymentCode, getPrefWidth());
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
setImage(paymentCodeCache.get(cacheId));
|
||||
} else {
|
||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
|
||||
payNymAvatarService.setOnRunning(runningEvent -> {
|
||||
getChildren().clear();
|
||||
});
|
||||
payNymAvatarService.setOnSucceeded(successEvent -> {
|
||||
setImage(payNymAvatarService.getValue());
|
||||
});
|
||||
payNymAvatarService.setOnFailed(failedEvent -> {
|
||||
log.error("Error", failedEvent.getSource().getException());
|
||||
});
|
||||
payNymAvatarService.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setImage(Image image) {
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
getChildren().add(circle);
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCodeProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<PaymentCode> paymentCodeProperty() {
|
||||
return paymentCodeProperty;
|
||||
}
|
||||
|
||||
public void setPaymentCode(PaymentCode paymentCode) {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
|
||||
private static String getCacheId(PaymentCode paymentCode, double width) {
|
||||
return paymentCode.toString();
|
||||
}
|
||||
|
||||
private static class PayNymAvatarService extends Service<Image> {
|
||||
private final PaymentCode paymentCode;
|
||||
private final double width;
|
||||
|
||||
public PayNymAvatarService(PaymentCode paymentCode, double width) {
|
||||
this.paymentCode = paymentCode;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Image> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Image call() throws Exception {
|
||||
String paymentCodeStr = paymentCode.toString();
|
||||
String cacheId = getCacheId(paymentCode, width);
|
||||
|
||||
Object lock = paymentCodeLoading.get(cacheId);
|
||||
if(lock != null) {
|
||||
synchronized(lock) {
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
return paymentCodeCache.get(cacheId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lock = new Object();
|
||||
paymentCodeLoading.put(cacheId, lock);
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
return image;
|
||||
} catch(Exception e) {
|
||||
log.debug("Error loading PayNym avatar", e);
|
||||
throw e;
|
||||
} finally {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymController;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class PayNymCell extends ListCell<PayNym> {
|
||||
private final PayNymController payNymController;
|
||||
|
||||
public PayNymCell(PayNymController payNymController) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
getStyleClass().add("paynym-cell");
|
||||
setPrefHeight(50);
|
||||
this.payNymController = payNymController;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(PayNym payNym, boolean empty) {
|
||||
super.updateItem(payNym, empty);
|
||||
|
||||
getStyleClass().remove("unlinked");
|
||||
|
||||
if(empty || payNym == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(5, 5,5, 5));
|
||||
|
||||
PayNymAvatar payNymAvatar = new PayNymAvatar();
|
||||
payNymAvatar.setPrefWidth(30);
|
||||
payNymAvatar.setPrefHeight(30);
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
|
||||
HBox labelBox = new HBox();
|
||||
labelBox.setAlignment(Pos.CENTER);
|
||||
Label label = new Label(payNym.nymName(), payNymAvatar);
|
||||
label.setGraphicTextGap(10);
|
||||
labelBox.getChildren().add(label);
|
||||
pane.setLeft(labelBox);
|
||||
|
||||
if(getListView().getUserData() == Boolean.TRUE) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
Button button = new Button("Add Contact");
|
||||
hBox.getChildren().add(button);
|
||||
pane.setRight(hBox);
|
||||
button.setOnAction(event -> {
|
||||
button.setDisable(true);
|
||||
payNymController.followPayNym(payNym.paymentCode());
|
||||
});
|
||||
} else if(payNymController != null) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
pane.setRight(hBox);
|
||||
|
||||
if(payNymController.isLinked(payNym)) {
|
||||
Label linkedLabel = new Label("Linked", getLinkGlyph());
|
||||
linkedLabel.setTooltip(new Tooltip("You can send non-collaboratively to this contact."));
|
||||
hBox.getChildren().add(linkedLabel);
|
||||
} else {
|
||||
Button linkButton = new Button("Link Contact", getLinkGlyph());
|
||||
linkButton.setTooltip(new Tooltip("Create a transaction that will enable you to send non-collaboratively to this contact."));
|
||||
hBox.getChildren().add(linkButton);
|
||||
linkButton.setOnAction(event -> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setText(null);
|
||||
setGraphic(pane);
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getLinkGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LINK);
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
|
||||
public class PaymentCodeTextField extends CopyableTextField {
|
||||
private String paymentCodeStr;
|
||||
|
||||
public void setPaymentCode(PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCopyText() {
|
||||
return paymentCodeStr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private static final Logger log = LoggerFactory.getLogger(PrivateKeySweepDialog.class);
|
||||
|
||||
private final TextArea key;
|
||||
private final ComboBox<ScriptType> keyScriptType;
|
||||
private final CopyableLabel keyAddress;
|
||||
private final ComboBoxTextField toAddress;
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Sweep Private Key");
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field keyField = new Field();
|
||||
keyField.setText("Private Key:");
|
||||
key = new TextArea();
|
||||
key.setWrapText(true);
|
||||
key.setPromptText("Wallet Import Format (WIF)");
|
||||
key.setPrefRowCount(2);
|
||||
key.getStyleClass().add("fixed-width");
|
||||
HBox keyBox = new HBox(5);
|
||||
VBox keyButtonBox = new VBox(5);
|
||||
Button scanKey = new Button("", getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
scanKey.setOnAction(event -> scanPrivateKey());
|
||||
Button readKey = new Button("", getGlyph(FontAwesome5.Glyph.FILE_IMPORT));
|
||||
readKey.setOnAction(event -> readPrivateKey());
|
||||
keyButtonBox.getChildren().addAll(scanKey, readKey);
|
||||
keyBox.getChildren().addAll(key, keyButtonBox);
|
||||
HBox.setHgrow(key, Priority.ALWAYS);
|
||||
keyField.getInputs().add(keyBox);
|
||||
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||
|
||||
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
keyAddress = new CopyableLabel();
|
||||
keyAddress.getStyleClass().add("fixed-width");
|
||||
addressField.getInputs().add(keyAddress);
|
||||
|
||||
Field toAddressField = new Field();
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||
toAddress.setComboProperty(toWallet);
|
||||
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().addAll(toWallet, toAddress);
|
||||
toAddressField.getInputs().add(stackPane);
|
||||
|
||||
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ButtonType createButtonType = new javafx.scene.control.ButtonType("Create Transaction", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, createButtonType);
|
||||
|
||||
Button createButton = (Button) dialogPane.lookupButton(createButtonType);
|
||||
createButton.setDefaultButton(true);
|
||||
createButton.setDisable(true);
|
||||
createButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
createTransaction();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
key.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean isValidKey = isValidKey();
|
||||
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||
if(isValidKey) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(isValidKey()) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> null);
|
||||
dialogPane.setPrefWidth(680);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(key, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid private Key", !key.getText().isEmpty() && !isValidKey()));
|
||||
validationSupport.registerValidator(toAddress, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !toAddress.getText().isEmpty() && !isValidToAddress()));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isValidKey() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private DumpedPrivateKey getPrivateKey() {
|
||||
return DumpedPrivateKey.fromBase58(key.getText());
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Address getToAddress() throws InvalidAddressException {
|
||||
return Address.fromString(toAddress.getText());
|
||||
}
|
||||
|
||||
private void setFromAddress() {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address address = scriptType.getAddress(privateKey.getKey());
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void scanPrivateKey() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> result = qrScanDialog.showAndWait();
|
||||
if(result.isPresent() && result.get().payload != null) {
|
||||
key.setText(result.get().payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void readPrivateKey() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Private Key File");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
if(file.length() > 1024) {
|
||||
AppServices.showErrorDialog("Invalid private key file", "This file does not contain a valid private key.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
key.setText(Files.asCharSource(file, StandardCharsets.UTF_8).read().trim());
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Error reading private key file", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
addressUtxosService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating sweep transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
|
||||
Transaction noFeeTransaction = new Transaction();
|
||||
long total = 0;
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
total += txOutput.getValue();
|
||||
}
|
||||
|
||||
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
|
||||
noFeeTransaction.addOutput(sweepOutput);
|
||||
|
||||
Double feeRate = AppServices.getDefaultFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
transaction.setVersion(2);
|
||||
transaction.setLocktime(AppServices.getCurrentBlockHeight() == null ? 0 : AppServices.getCurrentBlockHeight());
|
||||
for(TransactionInput txInput : noFeeTransaction.getInputs()) {
|
||||
transaction.addInput(txInput);
|
||||
}
|
||||
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
|
||||
|
||||
PSBT psbt = new PSBT(transaction);
|
||||
for(int i = 0; i < txOutputs.size(); i++) {
|
||||
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||
TransactionInput txInput = transaction.getInputs().get(i);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
|
||||
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
|
||||
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
|
||||
}
|
||||
|
||||
if(txInput.getWitness() != null) {
|
||||
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||
}
|
||||
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
}
|
||||
|
||||
setResult(psbt.extractTransaction());
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||
glyph.setFontSize(12);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class ProgressTimer extends ProgressIndicator {
|
||||
private final IntegerProperty secondsProperty = new SimpleIntegerProperty(60);
|
||||
|
||||
private Timeline timeline;
|
||||
|
||||
public ProgressTimer() {
|
||||
super(0);
|
||||
getStyleClass().add("progress-timer");
|
||||
}
|
||||
|
||||
public void start() {
|
||||
start(e -> {});
|
||||
}
|
||||
|
||||
public void start(EventHandler<ActionEvent> onFinished) {
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
|
||||
new KeyFrame(Duration.seconds(getSeconds()), onFinished, new KeyValue(progressProperty(), 1)));
|
||||
timeline.setCycleCount(1);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if(timeline != null) {
|
||||
timeline.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public int getSeconds() {
|
||||
return secondsProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty secondsProperty() {
|
||||
return secondsProperty;
|
||||
}
|
||||
|
||||
public void setSeconds(int secondsProperty) {
|
||||
this.secondsProperty.set(secondsProperty);
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,12 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 100;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 250;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 400;
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
private final UR ur;
|
||||
private final UREncoder encoder;
|
||||
@@ -91,8 +94,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(550);
|
||||
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
||||
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
|
||||
}
|
||||
@@ -113,8 +117,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(550);
|
||||
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
||||
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
|
||||
}
|
||||
@@ -135,7 +140,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private Image getQrCode(String fragment) {
|
||||
try {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, 480, 480);
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
|
||||
@@ -225,19 +230,15 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
|
||||
if(useLegacyEncoding) {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, "success"));
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(12);
|
||||
if(styleClass != null) {
|
||||
glyph.getStyleClass().add(styleClass);
|
||||
}
|
||||
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
@@ -10,10 +10,13 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -23,14 +26,22 @@ import com.sparrowwallet.hummingbird.ResultType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -40,6 +51,7 @@ import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -53,39 +65,112 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private final WebcamService webcamService;
|
||||
private List<String> parts;
|
||||
|
||||
private boolean isUr;
|
||||
private QRScanDialog.Result result;
|
||||
|
||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||
|
||||
private static final int SCAN_PERIOD_MILLIS = 100;
|
||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
||||
|
||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||
|
||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
|
||||
this.webcamService = new WebcamService(WebcamResolution.VGA);
|
||||
if(Config.get().isHdCapture()) {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
}
|
||||
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new QRScanDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(webcamView.getView());
|
||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setMinHeight(20);
|
||||
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
||||
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
||||
progressBar.progressProperty().bind(percentComplete);
|
||||
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.getChildren().addAll(wrappedView, progressBar);
|
||||
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
webcamService.resultProperty().addListener(new QRResultListener());
|
||||
webcamService.setOnFailed(failedEvent -> {
|
||||
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
|
||||
Throwable exception = failedEvent.getSource().getException();
|
||||
|
||||
Throwable nested = exception;
|
||||
while(nested.getCause() != null) {
|
||||
nested = nested.getCause();
|
||||
}
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||
Config.get().setWebcamDevice(null);
|
||||
}
|
||||
|
||||
final Throwable result = exception;
|
||||
Platform.runLater(() -> setResult(new Result(result)));
|
||||
});
|
||||
webcamService.start();
|
||||
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||
if(newResolution != null) {
|
||||
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
||||
}
|
||||
webcamService.cancel();
|
||||
});
|
||||
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
Platform.runLater(webcamService::cancel);
|
||||
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
if(Config.get().isHdCapture() != isHdCapture) {
|
||||
Config.get().setHdCapture(isHdCapture);
|
||||
}
|
||||
|
||||
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
||||
});
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.setPrefWidth(660);
|
||||
dialogPane.setPrefHeight(550);
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
|
||||
}
|
||||
@@ -101,11 +186,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
String qrtext = qrResult.getText();
|
||||
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
|
||||
|
||||
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
isUr = true;
|
||||
|
||||
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext.toLowerCase());
|
||||
legacyDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
||||
|
||||
if(legacyDecoder.isComplete()) {
|
||||
try {
|
||||
@@ -117,6 +201,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
} else {
|
||||
decoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
@@ -138,12 +223,20 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
parts.set(m - 1, payload);
|
||||
|
||||
if(n > 0) {
|
||||
Platform.runLater(() -> percentComplete.setValue((double)parts.stream().filter(Objects::nonNull).count() / n));
|
||||
}
|
||||
|
||||
if(parts.stream().filter(Objects::nonNull).count() == n) {
|
||||
String complete = String.join("", parts);
|
||||
try {
|
||||
PSBT psbt = PSBT.fromString(complete);
|
||||
PSBT psbt = PSBT.fromString(complete, false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(complete)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
@@ -156,7 +249,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
//ignore, bytes not parsable as tx
|
||||
}
|
||||
|
||||
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
|
||||
result = new Result(complete);
|
||||
}
|
||||
} else {
|
||||
PSBT psbt;
|
||||
@@ -189,15 +282,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
|
||||
try {
|
||||
psbt = PSBT.fromString(qrtext);
|
||||
psbt = PSBT.fromString(qrtext, false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(qrtext)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as Base64 or hex
|
||||
}
|
||||
|
||||
try {
|
||||
psbt = new PSBT(qrResult.getRawBytes());
|
||||
psbt = new PSBT(qrResult.getRawBytes(), false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
@@ -222,7 +319,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
//Try Base43 used by Electrum
|
||||
try {
|
||||
psbt = new PSBT(Base43.decode(qrtext));
|
||||
psbt = new PSBT(Base43.decode(qrtext), false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
@@ -248,8 +345,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(urRegistryType.equals(RegistryType.BYTES)) {
|
||||
byte[] urBytes = (byte[])ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(urBytes);
|
||||
PSBT psbt = new PSBT(urBytes, false);
|
||||
return new Result(psbt);
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(urBytes)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
@@ -274,7 +375,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
|
||||
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
|
||||
PSBT psbt = new PSBT(cryptoPSBT.getPsbt(), false);
|
||||
return new Result(psbt);
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
|
||||
@@ -380,25 +481,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
|
||||
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
|
||||
Wallet wallet = new Wallet();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
if(outputDescriptor.isMultisig()) {
|
||||
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
|
||||
}
|
||||
|
||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
return wallets;
|
||||
}
|
||||
|
||||
private ScriptType getScriptType(List<ScriptExpression> expressions) {
|
||||
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
|
||||
List<ScriptExpression> expressions = new ArrayList<>(scriptExpressions);
|
||||
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG
|
||||
|| expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG
|
||||
|| expressions.get(expressions.size() - 1) == ScriptExpression.COSIGNER) {
|
||||
expressions.remove(expressions.size() - 1);
|
||||
}
|
||||
|
||||
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
|
||||
return ScriptType.P2PKH;
|
||||
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||
@@ -411,6 +509,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
return ScriptType.P2SH_P2WSH;
|
||||
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||
return ScriptType.P2WSH;
|
||||
} else if(List.of(ScriptExpression.TAPROOT).equals(expressions)) {
|
||||
return ScriptType.P2TR;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown script of " + expressions);
|
||||
@@ -425,6 +525,100 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanListener implements WebcamListener {
|
||||
@Override
|
||||
public void webcamOpen(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||
if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
if(!webcamService.isRunning()) {
|
||||
webcamService.reset();
|
||||
webcamService.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamDisposed(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamImageObtained(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button = null;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton hd = new ToggleButton(buttonType.getText());
|
||||
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
hd.setGraphicTextGap(5);
|
||||
setHdGraphic(hd, hd.isSelected());
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(hd, buttonData);
|
||||
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
|
||||
setHdGraphic(hd, newValue);
|
||||
});
|
||||
|
||||
button = hd;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||
devicesCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WebcamDevice device) {
|
||||
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebcamDevice fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||
|
||||
button = devicesCombo;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
if(button instanceof Region) {
|
||||
((Region)button).setPrefWidth(150);
|
||||
((Region)button).setMaxWidth(150);
|
||||
}
|
||||
|
||||
button.disableProperty().bind(webcamService.openingProperty());
|
||||
return button;
|
||||
}
|
||||
|
||||
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
||||
if(isHd) {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
public final Transaction transaction;
|
||||
public final PSBT psbt;
|
||||
@@ -570,4 +764,46 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebcamDependencyException extends ScanException {
|
||||
public WebcamDependencyException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WebcamDependencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebcamDependencyException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public WebcamDependencyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebcamOpenException extends ScanException {
|
||||
public WebcamOpenException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebcamOpenException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
|
||||
private final List<WalletForm> walletForms;
|
||||
private final TextField search;
|
||||
private final CoinTreeTable results;
|
||||
|
||||
public SearchWalletDialog(List<WalletForm> walletForms) {
|
||||
this.walletForms = walletForms;
|
||||
|
||||
if(walletForms.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to search");
|
||||
}
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Search Wallet");
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field searchField = new Field();
|
||||
searchField.setText("Search:");
|
||||
search = TextFields.createClearableTextField();
|
||||
search.setPromptText("Label, address, value or transaction ID");
|
||||
searchField.getInputs().add(search);
|
||||
|
||||
fieldset.getChildren().addAll(searchField);
|
||||
form.getChildren().add(fieldset);
|
||||
|
||||
boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty());
|
||||
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet ? 950 : 850);
|
||||
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName());
|
||||
});
|
||||
results.getColumns().add(walletColumn);
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> typeColumn = new TreeTableColumn<>("Type");
|
||||
typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType());
|
||||
});
|
||||
results.getColumns().add(typeColumn);
|
||||
|
||||
TreeTableColumn<Entry, Entry> entryCol = new TreeTableColumn<>("Date / Address / Output");
|
||||
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
entryCol.setCellFactory(p -> new SearchEntryCell());
|
||||
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
results.getColumns().add(entryCol);
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||
});
|
||||
amountCol.setCellFactory(p -> new CoinCell());
|
||||
results.getColumns().add(amountCol);
|
||||
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
|
||||
|
||||
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||
showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty()
|
||||
|| walletForms.stream().map(WalletForm::getWallet).noneMatch(wallet -> wallet == results.getSelectionModel().getSelectedItem().getValue().getWallet()));
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallet(newValue.toLowerCase());
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallet(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = null;
|
||||
|
||||
try {
|
||||
searchValue = Math.abs(Long.parseLong(searchText));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return "Search Wallet Results";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchEntryCell extends EntryCell {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore);
|
||||
MnemonicKeystoreDisplayPane keystorePane = new MnemonicKeystoreDisplayPane(decryptedKeystore);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
@@ -44,6 +44,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(150 + height);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvReader;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
setTitle("Send to Many");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView.getColumns().get(0).setPrefWidth(400);
|
||||
spreadsheetView.getColumns().get(1).setPrefWidth(150);
|
||||
spreadsheetView.getColumns().get(2).setPrefWidth(247);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(spreadsheetView);
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
private Grid getGrid(List<Payment> payments) {
|
||||
int rowCount = payments.size();
|
||||
int columnCount = 3;
|
||||
GridBase grid = new GridBase(rowCount, columnCount);
|
||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||
|
||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
double amount = (double)payments.get(row).getAmount();
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||
amountCell.getStyleClass().add("number-value");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
amountCell.getStyleClass().add("number-field");
|
||||
}
|
||||
list.add(amountCell);
|
||||
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||
rows.add(list);
|
||||
}
|
||||
grid.setRows(rows);
|
||||
grid.getColumnHeaders().setAll("Address", "Amount (" + bitcoinUnit.getLabel() + ")", "Label");
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
Address address = (Address)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(address != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(new Payment(address, label, value.longValue(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
private class SendToManyDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button loadButton = new Button(buttonType.getText());
|
||||
loadButton.setGraphicTextGap(5);
|
||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(loadButton, buttonData);
|
||||
loadButton.setOnAction(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open CSV");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("CSV", "*.csv")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
|
||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||
if(file != null) {
|
||||
try {
|
||||
List<Payment> csvPayments = new ArrayList<>();
|
||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||
CsvReader csvReader = new CsvReader(reader);
|
||||
while(csvReader.readRecord()) {
|
||||
if(csvReader.getColumnCount() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
long amount;
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
|
||||
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
} else {
|
||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||
}
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
String label = csvReader.get(2);
|
||||
csvPayments.add(new Payment(address, label, amount, false));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore and continue - probably a header line
|
||||
} catch(InvalidAddressException e) {
|
||||
AppServices.showErrorDialog("Invalid Address", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if(csvPayments.isEmpty()) {
|
||||
AppServices.showErrorDialog("No recipients found", "No valid recipients were found. Use a CSV file with three columns, and ensure amounts are in " + bitcoinUnit.getLabel() + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = loadButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||
public AddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address fromString(String str) {
|
||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||
return null;
|
||||
} else {
|
||||
return myConverter.fromString(str);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Address item, String format) {
|
||||
try {
|
||||
if(item == null) {
|
||||
return ""; //$NON-NLS-1$
|
||||
} else {
|
||||
return item.toString();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
return myConverter.toString(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public AddressCellType(StringConverter<Address> converter) {
|
||||
super(converter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "address";
|
||||
}
|
||||
|
||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||
final Address value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
|
||||
return new SpreadsheetCellEditor.StringEditor(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value instanceof Address)
|
||||
return true;
|
||||
else {
|
||||
try {
|
||||
converter.fromString(value == null ? null : value.toString());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address convertValue(Object value) {
|
||||
if(value instanceof Address)
|
||||
return (Address)value;
|
||||
else {
|
||||
try {
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item, String format) {
|
||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.controlsfx.dialog.ProgressDialog;
|
||||
|
||||
public class ServiceProgressDialog extends ProgressDialog {
|
||||
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
|
||||
super(worker);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle(title);
|
||||
setHeaderText(header);
|
||||
|
||||
dialogPane.getStyleClass().remove("progress-dialog");
|
||||
Image image = new Image(imagePath);
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class TextAreaDialog extends Dialog<String> {
|
||||
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
|
||||
|
||||
private final TextArea textArea;
|
||||
private final String defaultValue;
|
||||
|
||||
@@ -15,14 +27,23 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
this("");
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
public TextAreaDialog(String defaultValue) {
|
||||
this(defaultValue, true);
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
|
||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
HBox hbox = new HBox();
|
||||
this.textArea = new TextArea(defaultValue);
|
||||
this.textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
this.textArea.setWrapText(true);
|
||||
this.textArea.getStyleClass().add("fixed-width");
|
||||
this.textArea.setEditable(editable);
|
||||
hbox.getChildren().add(textArea);
|
||||
HBox.setHgrow(this.textArea, Priority.ALWAYS);
|
||||
|
||||
@@ -33,7 +54,13 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
dialogPane.getStyleClass().add("text-input-dialog");
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
dialogPane.getButtonTypes().add(ButtonType.OK);
|
||||
if(editable) {
|
||||
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
|
||||
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
Platform.runLater(textArea::requestFocus);
|
||||
|
||||
@@ -44,6 +71,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
|
||||
dialogPane.setPrefWidth(700);
|
||||
dialogPane.setPrefHeight(400);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public final TextArea getEditor() {
|
||||
@@ -53,4 +81,54 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
public final String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private class TextAreaDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button scanButton = new Button(buttonType.getText());
|
||||
scanButton.setGraphicTextGap(5);
|
||||
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(scanButton, buttonData);
|
||||
scanButton.setOnAction(event -> {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.payload != null) {
|
||||
textArea.setText(result.payload);
|
||||
} else if(result.psbt != null) {
|
||||
textArea.setText(result.psbt.toBase64String());
|
||||
} else if(result.transaction != null) {
|
||||
textArea.setText(Utils.bytesToHex(result.transaction.bitcoinSerialize()));
|
||||
} else if(result.uri != null) {
|
||||
textArea.setText(result.uri.toString());
|
||||
} else if(result.extendedKey != null) {
|
||||
textArea.setText(result.extendedKey.getExtendedKey());
|
||||
} else if(result.outputDescriptor != null) {
|
||||
textArea.setText(result.outputDescriptor.toString(true));
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = scanButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,18 @@ public class TextUtils {
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static double computeTextHeight(Font font, String text) {
|
||||
helper.setText(text);
|
||||
helper.setFont(font);
|
||||
|
||||
helper.setWrappingWidth(0.0D);
|
||||
helper.setLineSpacing(0.0D);
|
||||
double d = Math.ceil(helper.getLayoutBounds().getHeight());
|
||||
|
||||
helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
|
||||
helper.setLineSpacing(DEFAULT_LINE_SPACING);
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.Arrays;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public class TitledDescriptionPane extends TitledPane {
|
||||
private Label mainLabel;
|
||||
private Label descriptionLabel;
|
||||
protected Hyperlink showHideLink;
|
||||
protected HBox buttonBox;
|
||||
@@ -50,7 +51,7 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
VBox labelsBox = new VBox();
|
||||
labelsBox.setSpacing(5);
|
||||
labelsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Label mainLabel = new Label();
|
||||
mainLabel = new Label();
|
||||
mainLabel.setText(title);
|
||||
mainLabel.getStyleClass().add("main-label");
|
||||
labelsBox.getChildren().add(mainLabel);
|
||||
@@ -61,7 +62,7 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
|
||||
descriptionLabel = new Label(description);
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
showHideLink = new Hyperlink("Show Details...");
|
||||
showHideLink = new Hyperlink("Details...");
|
||||
showHideLink.managedProperty().bind(showHideLink.visibleProperty());
|
||||
showHideLink.setOnAction(event -> {
|
||||
setExpanded(!this.isExpanded());
|
||||
@@ -99,6 +100,10 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mainLabel.getText();
|
||||
}
|
||||
|
||||
protected void setDescription(String text) {
|
||||
descriptionLabel.getStyleClass().remove("description-error");
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
public class TorStatusLabel extends Label {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorStatusLabel.class);
|
||||
|
||||
private final TorConnectionTest torConnectionTest = new TorConnectionTest();
|
||||
|
||||
public TorStatusLabel() {
|
||||
getStyleClass().add("tor-status");
|
||||
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
||||
setGraphic(getIcon());
|
||||
update();
|
||||
}
|
||||
|
||||
public void update() {
|
||||
boolean proxyInUse = AppServices.isUsingProxy();
|
||||
boolean internal = AppServices.isTorRunning();
|
||||
|
||||
if(!proxyInUse || internal) {
|
||||
torConnectionTest.cancel();
|
||||
if(internal) {
|
||||
setTooltip(new Tooltip("Internal Tor proxy enabled"));
|
||||
}
|
||||
} else if(!torConnectionTest.isRunning()) {
|
||||
torConnectionTest.setPeriod(Duration.seconds(20.0));
|
||||
torConnectionTest.setBackoffStrategy(null);
|
||||
torConnectionTest.setOnSucceeded(workerStateEvent -> {
|
||||
getStyleClass().remove("failure");
|
||||
setTooltip(new Tooltip("External Tor proxy enabled"));
|
||||
});
|
||||
torConnectionTest.setOnFailed(workerStateEvent -> {
|
||||
if(!getStyleClass().contains("failure")) {
|
||||
getStyleClass().add("failure");
|
||||
}
|
||||
setTooltip(new Tooltip("External Tor proxy error: " + workerStateEvent.getSource().getException().getMessage()));
|
||||
log.warn("Failed to connect to external Tor proxy: " + workerStateEvent.getSource().getException().getMessage());
|
||||
});
|
||||
torConnectionTest.start();
|
||||
}
|
||||
}
|
||||
|
||||
private Node getIcon() {
|
||||
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
|
||||
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
|
||||
adjust.setRotate(180);
|
||||
|
||||
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
|
||||
bullseye.setFontSize(14);
|
||||
|
||||
Group group = new Group();
|
||||
group.getChildren().addAll(adjust, bullseye);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static class TorConnectionTest extends ScheduledService<Void> {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() throws IOException {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(Config.get().getProxyServer());
|
||||
Socket socket = new Socket(proxyHostAndPort.getHost(), proxyHostAndPort.getPort());
|
||||
socket.close();
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,26 +3,44 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.IndexRange;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.stage.Popup;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
import org.fxmisc.richtext.event.MouseOverTextEvent;
|
||||
import org.fxmisc.richtext.model.StyleSpan;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
|
||||
|
||||
public class TransactionHexArea extends CodeArea {
|
||||
private static final int TRUNCATE_AT = 30000;
|
||||
private static final int SEGMENTS_INTERVAL = 250;
|
||||
|
||||
private String fullHex;
|
||||
private List<TransactionSegment> previousSegmentList = new ArrayList<>();
|
||||
|
||||
public TransactionHexArea() {
|
||||
super();
|
||||
addPopupHandler();
|
||||
}
|
||||
|
||||
public void setTransaction(Transaction transaction) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
transaction.bitcoinSerializeToStream(baos);
|
||||
|
||||
String hex = Utils.bytesToHex(baos.toByteArray());
|
||||
fullHex = Utils.bytesToHex(baos.toByteArray());
|
||||
String hex = fullHex;
|
||||
if(hex.length() > TRUNCATE_AT) {
|
||||
hex = hex.substring(0, TRUNCATE_AT);
|
||||
hex += "[truncated]";
|
||||
@@ -31,6 +49,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
clear();
|
||||
appendText(hex);
|
||||
previousSegmentList = new ArrayList<>();
|
||||
setContextMenu(new TransactionHexContextMenu(fullHex));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Can't happen");
|
||||
}
|
||||
@@ -44,12 +63,33 @@ public class TransactionHexArea extends CodeArea {
|
||||
previousSegmentList = segments;
|
||||
}
|
||||
|
||||
private void addPopupHandler() {
|
||||
Popup popup = new Popup();
|
||||
Label popupMsg = new Label();
|
||||
popupMsg.getStyleClass().add("tooltip");
|
||||
popup.getContent().add(popupMsg);
|
||||
|
||||
setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
Position position = getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex() + 1, Backward);
|
||||
StyleSpan<Collection<String>> styleSpan = getParagraph(0).getStyleSpans().getStyleSpan(position.getMajor());
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeTransactionPart(styleSpan.getStyle()));
|
||||
if(!popupMsg.getText().isEmpty()) {
|
||||
popup.show(this, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
});
|
||||
addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
||||
popup.hide();
|
||||
});
|
||||
}
|
||||
|
||||
private void applyHighlighting(int start, List<TransactionSegment> segments) {
|
||||
int end = Math.min(segments.size(), start + SEGMENTS_INTERVAL);
|
||||
for(int i = start; i < end; i++) {
|
||||
TransactionSegment segment = segments.get(i);
|
||||
if(segment.start < TRUNCATE_AT) {
|
||||
setStyleClass(segment.start, Math.min(TRUNCATE_AT, segment.start + segment.length), segment.style);
|
||||
setStyle(segment.start, Math.min(TRUNCATE_AT, segment.start + segment.length), getStyles(segment));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +100,18 @@ public class TransactionHexArea extends CodeArea {
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<String> getStyles(TransactionSegment segment) {
|
||||
List<String> styles = new ArrayList<>();
|
||||
styles.add(segment.style);
|
||||
if(segment.index != null) {
|
||||
styles.add("index-" + segment.index);
|
||||
}
|
||||
if(segment.witnessIndex != null) {
|
||||
styles.add("witnessindex-" + segment.witnessIndex);
|
||||
}
|
||||
return Collections.unmodifiableList(styles);
|
||||
}
|
||||
|
||||
public List<TransactionSegment> getTransactionSegments(Transaction transaction, int selectedInputIndex, int selectedOutputIndex) {
|
||||
List<TransactionSegment> segments = new ArrayList<>();
|
||||
|
||||
@@ -82,12 +134,12 @@ public class TransactionHexArea extends CodeArea {
|
||||
//Inputs
|
||||
for(int i = 0; i < transaction.getInputs().size(); i++) {
|
||||
TransactionInput input = transaction.getInputs().get(i);
|
||||
cursor = addSegment(segments, cursor, 32 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "hash"));
|
||||
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "index"));
|
||||
cursor = addSegment(segments, cursor, 32 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "hash"));
|
||||
cursor = addSegment(segments, cursor, 4 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "index"));
|
||||
VarInt scriptLen = new VarInt(input.getScriptBytes().length);
|
||||
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript-length"));
|
||||
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript"));
|
||||
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sequence"));
|
||||
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript-length"));
|
||||
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript"));
|
||||
cursor = addSegment(segments, cursor, 4 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sequence"));
|
||||
}
|
||||
|
||||
//Number of outputs
|
||||
@@ -97,10 +149,10 @@ public class TransactionHexArea extends CodeArea {
|
||||
//Outputs
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
TransactionOutput output = transaction.getOutputs().get(i);
|
||||
cursor = addSegment(segments, cursor, 8 * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "value"));
|
||||
cursor = addSegment(segments, cursor, 8 * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "value"));
|
||||
VarInt scriptLen = new VarInt(output.getScriptBytes().length);
|
||||
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript-length"));
|
||||
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript"));
|
||||
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript-length"));
|
||||
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript"));
|
||||
}
|
||||
|
||||
if(transaction.hasWitnesses()) {
|
||||
@@ -109,11 +161,12 @@ public class TransactionHexArea extends CodeArea {
|
||||
if (input.hasWitness()) {
|
||||
TransactionWitness witness = input.getWitness();
|
||||
VarInt witnessCount = new VarInt(witness.getPushCount());
|
||||
cursor = addSegment(segments, cursor, witnessCount.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "count"));
|
||||
for (byte[] push : witness.getPushes()) {
|
||||
cursor = addSegment(segments, cursor, witnessCount.getSizeInBytes() * 2, i, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "count"));
|
||||
for(int j = 0; j < witness.getPushes().size(); j++) {
|
||||
byte[] push = witness.getPushes().get(j);
|
||||
VarInt witnessLen = new VarInt(push.length);
|
||||
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
|
||||
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +184,15 @@ public class TransactionHexArea extends CodeArea {
|
||||
}
|
||||
|
||||
private int addSegment(List<TransactionSegment> segments, int start, int length, String style) {
|
||||
segments.add(new TransactionSegment(start, length, style));
|
||||
return addSegment(segments, start, length, null, style);
|
||||
}
|
||||
|
||||
private int addSegment(List<TransactionSegment> segments, int start, int length, Integer index, String style) {
|
||||
return addSegment(segments, start, length, index, null, style);
|
||||
}
|
||||
|
||||
private int addSegment(List<TransactionSegment> segments, int start, int length, Integer index, Integer witnessIndex, String style) {
|
||||
segments.add(new TransactionSegment(start, length, index, witnessIndex, style));
|
||||
return start + length;
|
||||
}
|
||||
|
||||
@@ -143,15 +204,71 @@ public class TransactionHexArea extends CodeArea {
|
||||
return "other";
|
||||
}
|
||||
|
||||
private String describeTransactionPart(Collection<String> styles) {
|
||||
String style = "";
|
||||
Integer index = null;
|
||||
Integer witnessIndex = null;
|
||||
Iterator<String> iter = styles.iterator();
|
||||
if(iter.hasNext()) {
|
||||
style = iter.next();
|
||||
}
|
||||
while(iter.hasNext()) {
|
||||
String indexStyle = iter.next();
|
||||
if(indexStyle.startsWith("index-")) {
|
||||
index = Integer.parseInt(indexStyle.substring("index-".length()));
|
||||
}
|
||||
if(indexStyle.startsWith("witnessindex-")) {
|
||||
witnessIndex = Integer.parseInt(indexStyle.substring("witnessindex-".length()));
|
||||
}
|
||||
}
|
||||
|
||||
return switch(style) {
|
||||
case "version" -> "Transaction version";
|
||||
case "segwit-marker" -> "Segwit marker";
|
||||
case "segwit-flag" -> "Segwit flag";
|
||||
case "num-inputs" -> "Number of inputs";
|
||||
case "input-hash" -> "Input #" + index + " outpoint txid";
|
||||
case "input-index" -> "Input #" + index + " outpoint index";
|
||||
case "input-sigscript-length" -> "Input #" + index + " scriptSig length";
|
||||
case "input-sigscript" -> "Input #" + index + " scriptSig";
|
||||
case "input-sequence" -> "Input #" + index + " sequence";
|
||||
case "num-outputs" -> "Number of outputs";
|
||||
case "output-value" -> "Output #" + index + " value";
|
||||
case "output-pubkeyscript-length" -> "Output #" + index + " scriptPubKey length";
|
||||
case "output-pubkeyscript" -> "Output #" + index + " scriptPubKey";
|
||||
case "witness-count" -> "Input #" + index + " witness count";
|
||||
case "witness-length" -> "Input #" + index + " witness #" + witnessIndex + " length";
|
||||
case "witness-data" -> "Input #" + index + " witness #" + witnessIndex + " data";
|
||||
case "locktime" -> "Locktime";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy() {
|
||||
IndexRange selection = getSelection();
|
||||
if(fullHex != null && selection.getLength() == getLength()) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(fullHex);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
} else {
|
||||
super.copy();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionSegment {
|
||||
public TransactionSegment(int start, int length, String style) {
|
||||
public TransactionSegment(int start, int length, Integer index, Integer witnessIndex, String style) {
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
this.index = index;
|
||||
this.witnessIndex = witnessIndex;
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
public int start;
|
||||
public int length;
|
||||
public Integer index;
|
||||
public Integer witnessIndex;
|
||||
public String style;
|
||||
|
||||
@Override
|
||||
@@ -169,4 +286,18 @@ public class TransactionHexArea extends CodeArea {
|
||||
return Objects.hash(start, length, style);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionHexContextMenu extends ContextMenu {
|
||||
public TransactionHexContextMenu(String hex) {
|
||||
MenuItem copy = new MenuItem("Copy All");
|
||||
copy.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hex);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(txid, Validator.combine(
|
||||
Validator.createEmptyValidator("Transaction id is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID length incorrect", newValue.length() != 64),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID must be hexadecimal", !Utils.isHex(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Open Transaction", ButtonBar.ButtonData.OK_DONE);
|
||||
@@ -63,5 +63,6 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
|
||||
txid.setPromptText("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16");
|
||||
Platform.runLater(txid::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? Sha256Hash.wrap(txid.getText()) : null);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
@@ -51,7 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
balanceCol.setSortable(true);
|
||||
getColumns().add(balanceCol);
|
||||
|
||||
setPlaceholder(new Label("No transactions"));
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
|
||||
@@ -72,7 +71,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
public void updateHistory() {
|
||||
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
|
||||
sort();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BaseMultiResolutionImage;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TrayManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(TrayManager.class);
|
||||
|
||||
private final SystemTray tray;
|
||||
private final TrayIcon trayIcon;
|
||||
private final PopupMenu popupMenu = new PopupMenu();
|
||||
|
||||
public TrayManager() {
|
||||
if(!SystemTray.isSupported()) {
|
||||
throw new UnsupportedOperationException("SystemTray icons are not supported by the current desktop environment.");
|
||||
}
|
||||
|
||||
tray = SystemTray.getSystemTray();
|
||||
|
||||
try {
|
||||
List<Image> imgList = new ArrayList<>();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
|
||||
} else {
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@2x.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@3x.png")));
|
||||
}
|
||||
|
||||
BaseMultiResolutionImage mrImage = new BaseMultiResolutionImage(imgList.toArray(new Image[0]));
|
||||
|
||||
this.trayIcon = new TrayIcon(mrImage, "Sparrow", popupMenu);
|
||||
|
||||
MenuItem miExit = new MenuItem("Quit Sparrow");
|
||||
miExit.addActionListener(e -> {
|
||||
SwingUtilities.invokeLater(() -> { tray.remove(this.trayIcon); });
|
||||
Platform.exit();
|
||||
});
|
||||
this.popupMenu.add(miExit);
|
||||
} catch(IOException e) {
|
||||
log.error("Could not load system tray image", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStage(Stage stage) {
|
||||
EventQueue.invokeLater(() -> {
|
||||
MenuItem miStage = new MenuItem(stage.getTitle());
|
||||
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
|
||||
miStage.addActionListener(e -> Platform.runLater(() -> {
|
||||
stage.show();
|
||||
EventQueue.invokeLater(() -> {
|
||||
popupMenu.remove(miStage);
|
||||
|
||||
if(popupMenu.getItemCount() == 1) {
|
||||
Platform.setImplicitExit(true);
|
||||
SwingUtilities.invokeLater(() -> tray.remove(trayIcon));
|
||||
}
|
||||
});
|
||||
}));
|
||||
//Make sure it's always at the top
|
||||
this.popupMenu.insert(miStage,popupMenu.getItemCount() - 1);
|
||||
|
||||
if(!isShowing()) {
|
||||
// Keeps the JVM running even if there are no
|
||||
// visible JavaFX Stages, otherwise JVM would
|
||||
// exit and we lose the TrayIcon
|
||||
Platform.setImplicitExit(false);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
tray.add(this.trayIcon);
|
||||
} catch(AWTException e) {
|
||||
log.error("Unable to add system tray icon", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return Arrays.stream(tray.getTrayIcons()).collect(Collectors.toList()).contains(trayIcon);
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Desktop.isDesktopSupported() && SystemTray.isSupported();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
@@ -22,7 +27,26 @@ public class UsbStatusButton extends MenuButton {
|
||||
public void setDevices(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(device.getNeedsPinSent()) {
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
toggleItem.setOnAction(event -> {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(event1 -> {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
if(!device.getModel().externalPassphraseEntry()) {
|
||||
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
|
||||
}
|
||||
});
|
||||
togglePassphraseService.setOnFailed(event1 -> {
|
||||
AppServices.showErrorDialog("Error toggling passphrase", event1.getSource().getException().getMessage());
|
||||
});
|
||||
togglePassphraseService.start();
|
||||
});
|
||||
((Menu)deviceItem).getItems().add(toggleItem);
|
||||
}
|
||||
|
||||
if(device.isNeedsPinSent()) {
|
||||
deviceItem.setGraphic(getLockIcon());
|
||||
} else {
|
||||
deviceItem.setGraphic(getLockOpenIcon());
|
||||
|
||||
@@ -9,14 +9,18 @@ import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxosChart extends BarChart<String, Number> {
|
||||
private static final int MAX_BARS = 8;
|
||||
private static final int MAX_BARS = 10;
|
||||
private static final String OTHER_CATEGORY = "Other";
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
|
||||
private List<Entry> selectedEntries;
|
||||
private int totalUtxos;
|
||||
@@ -44,20 +48,24 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
totalUtxos = utxoDataList.size();
|
||||
if(utxoDataList.size() > MAX_BARS) {
|
||||
Long otherTotal = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size()).stream().mapToLong(data -> data.getYValue().longValue()).sum();
|
||||
List<Data<String, Number>> otherDataList = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size());
|
||||
List<Entry> otherEntries = otherDataList.stream().map(data -> (Entry)data.getExtraValue()).collect(Collectors.toList());
|
||||
Long otherTotal = otherDataList.stream().mapToLong(data -> data.getYValue().longValue()).sum();
|
||||
utxoDataList = utxoDataList.subList(0, MAX_BARS - 1);
|
||||
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal));
|
||||
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal, otherEntries));
|
||||
}
|
||||
|
||||
for(int i = 0; i < utxoDataList.size(); i++) {
|
||||
XYChart.Data<String, Number> newData = utxoDataList.get(i);
|
||||
if(i < utxoSeries.getData().size()) {
|
||||
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() != null && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
utxoSeries.getData().set(i, newData);
|
||||
installTooltip(newData);
|
||||
}
|
||||
} else {
|
||||
utxoSeries.getData().add(newData);
|
||||
installTooltip(newData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +80,21 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
private String getCategoryName(Entry entry) {
|
||||
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
|
||||
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." : entry.getLabel();
|
||||
return entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
|
||||
}
|
||||
|
||||
return ((UtxoEntry)entry).getDescription();
|
||||
}
|
||||
|
||||
private void installTooltip(XYChart.Data<String, Number> item) {
|
||||
Tooltip.uninstall(item.getNode(), null);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", item.getYValue());
|
||||
Tooltip tooltip = new Tooltip(item.getXValue() + "\n" + satsValue + " sats");
|
||||
tooltip.setShowDelay(Duration.millis(TOOLTIP_SHOW_DELAY));
|
||||
Tooltip.install(item.getNode(), tooltip);
|
||||
}
|
||||
|
||||
public void select(List<Entry> entries) {
|
||||
Set<Node> selectedBars = lookupAll(".chart-bar.selected");
|
||||
for(Node selectedBar : selectedBars) {
|
||||
@@ -87,7 +104,8 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
for(int i = 0; i < utxoSeries.getData().size(); i++) {
|
||||
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
|
||||
|
||||
if((data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) || (data.getExtraValue() == null && entries.size() == totalUtxos)) {
|
||||
if((data.getExtraValue() instanceof Entry && entries.contains((Entry)data.getExtraValue())) ||
|
||||
(data.getExtraValue() instanceof List && entries.containsAll((List)data.getExtraValue()))) {
|
||||
Node bar = lookup(".data" + i);
|
||||
if(bar != null) {
|
||||
bar.getStyleClass().add("selected");
|
||||
|
||||
@@ -3,11 +3,11 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class UtxosTreeTable extends CoinTreeTable {
|
||||
@@ -39,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
});
|
||||
getColumns().add(outputCol);
|
||||
|
||||
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator((o1, o2) -> {
|
||||
UtxoEntry entry1 = (UtxoEntry)o1;
|
||||
UtxoEntry entry2 = (UtxoEntry)o2;
|
||||
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
|
||||
});
|
||||
getColumns().add(addressCol);
|
||||
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
|
||||
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
|
||||
});
|
||||
mixStatusCol.setCellFactory(p -> new MixStatusCell());
|
||||
mixStatusCol.setSortable(true);
|
||||
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
|
||||
getColumns().add(mixStatusCol);
|
||||
} else {
|
||||
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
|
||||
getColumns().add(addressCol);
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
@@ -69,7 +76,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
getColumns().add(amountCol);
|
||||
setTreeColumn(amountCol);
|
||||
|
||||
setPlaceholder(new Label("No unspent outputs"));
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);
|
||||
@@ -92,9 +99,11 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
public void updateHistory() {
|
||||
//Utxo entries should have already been updated, so only a resort required
|
||||
sort();
|
||||
if(!getRoot().getChildren().isEmpty()) {
|
||||
sort();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
|
||||
public class ViewPasswordField extends CustomPasswordField {
|
||||
public ViewPasswordField() {
|
||||
super();
|
||||
getStyleClass().add("view-password-text-field");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ViewPasswordFieldSkin(this) {
|
||||
@Override
|
||||
public ObjectProperty<Node> leftProperty() {
|
||||
return ViewPasswordField.this.leftProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
return ViewPasswordField.this.rightProperty();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import impl.org.controlsfx.skin.CustomTextFieldSkin;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
|
||||
public abstract class ViewPasswordFieldSkin extends CustomTextFieldSkin {
|
||||
private static final Duration FADE_DURATION = Duration.millis(350);
|
||||
public static final char BULLET = '\u25CF';
|
||||
|
||||
private boolean mask = true;
|
||||
|
||||
public ViewPasswordFieldSkin(CustomPasswordField textField) {
|
||||
super(textField);
|
||||
|
||||
Region viewPasswordButton = new Region();
|
||||
viewPasswordButton.getStyleClass().addAll("graphic");
|
||||
StackPane viewPasswordButtonPane = new StackPane(viewPasswordButton);
|
||||
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
|
||||
viewPasswordButtonPane.setOpacity(0.0);
|
||||
viewPasswordButtonPane.setCursor(Cursor.DEFAULT);
|
||||
|
||||
viewPasswordButtonPane.setOnMouseReleased(e -> {
|
||||
if(mask) {
|
||||
viewPasswordButtonPane.getStyleClass().remove("view-password-button");
|
||||
viewPasswordButtonPane.getStyleClass().addAll("hide-password-button");
|
||||
mask = false;
|
||||
} else {
|
||||
viewPasswordButtonPane.getStyleClass().remove("hide-password-button");
|
||||
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
|
||||
mask = true;
|
||||
}
|
||||
textField.setText(textField.getText());
|
||||
textField.end();
|
||||
});
|
||||
|
||||
textField.rightProperty().set(viewPasswordButtonPane);
|
||||
|
||||
final FadeTransition fader = new FadeTransition(FADE_DURATION, viewPasswordButtonPane);
|
||||
fader.setCycleCount(1);
|
||||
|
||||
textField.textProperty().addListener(new InvalidationListener() {
|
||||
@Override
|
||||
public void invalidated(Observable arg0) {
|
||||
String text = textField.getText();
|
||||
boolean isTextEmpty = text == null || text.isEmpty();
|
||||
boolean isButtonVisible = fader.getNode().getOpacity() > 0;
|
||||
|
||||
if (isTextEmpty && isButtonVisible) {
|
||||
setButtonVisible(false);
|
||||
} else if (!isTextEmpty && !isButtonVisible) {
|
||||
setButtonVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setButtonVisible( boolean visible ) {
|
||||
fader.setFromValue(visible? 0.0: 1.0);
|
||||
fader.setToValue(visible? 1.0: 0.0);
|
||||
fader.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String maskText(String txt) {
|
||||
if(getSkinnable() instanceof PasswordField && mask) {
|
||||
int n = txt.length();
|
||||
StringBuilder passwordBuilder = new StringBuilder(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
passwordBuilder.append(BULLET);
|
||||
}
|
||||
return passwordBuilder.toString();
|
||||
} else {
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user