mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 05:31:23 +00:00
Compare commits
2399 Commits
v1.0
...
online-acc
Author | SHA1 | Date | |
---|---|---|---|
|
063ef8507b | ||
|
f042b5ca5f | ||
|
a10e669554 | ||
|
501f66ab00 | ||
|
6003ed3ff7 | ||
|
03e3619817 | ||
|
0e42e7b05a | ||
|
d4fbc1687b | ||
|
8ffdc9b369 | ||
|
c883dd44c8 | ||
|
667530e202 | ||
|
5807d6e0dc | ||
|
ba4eeed358 | ||
|
82edc4d9f3 | ||
|
2a0d5746e6 | ||
|
23423102e7 | ||
|
8879ec5bb4 | ||
|
8cca6db316 | ||
|
effe1ac44d | ||
|
ad4308afdf | ||
|
6cfd85bdce | ||
|
8b61247712 | ||
|
a9267760eb | ||
|
0b8fcc0a7b | ||
|
3d3ecbfb15 | ||
|
9658f0cdd4 | ||
|
b23500fdd0 | ||
|
a1365e57d8 | ||
|
d8ca3a455d | ||
|
dcc943a906 | ||
|
cd2010bd06 | ||
|
8cd16792a2 | ||
|
4d97586f82 | ||
|
3612fd8257 | ||
|
ff96868bd9 | ||
|
1694d4552e | ||
|
bb1593efd2 | ||
|
4140546afb | ||
|
19197812d3 | ||
|
168d32a474 | ||
|
a4fade0157 | ||
|
2ea6921b66 | ||
|
11ef31215b | ||
|
830a608b14 | ||
|
57acf7dffe | ||
|
9debebe03e | ||
|
b17e96e121 | ||
|
b46c3cf95f | ||
|
86526507a6 | ||
|
1b9128289f | ||
|
4a58f90223 | ||
|
e68db40d91 | ||
|
bd6c0c9a7d | ||
|
5804b9469c | ||
|
53b47023ac | ||
|
22f9f08885 | ||
|
f26267e572 | ||
|
e8c29226a1 | ||
|
94f48f8f54 | ||
|
3aac580f2c | ||
|
2d0b035f98 | ||
|
075385d3ff | ||
|
6ed8250301 | ||
|
d10ff49dcb | ||
|
4cf34fa932 | ||
|
06b5d5f1d0 | ||
|
d6d2641cad | ||
|
e71f22fd2c | ||
|
c996633732 | ||
|
55f973af3c | ||
|
fe9744eec6 | ||
|
410fa59430 | ||
|
522ae2bce7 | ||
|
a6e79947b8 | ||
|
b9bf945fd8 | ||
|
85a27c14b8 | ||
|
46c40ca9ca | ||
|
fcd0d71cb6 | ||
|
275bee62d9 | ||
|
97221a4449 | ||
|
508a34684b | ||
|
3d2144f303 | ||
|
3c7fbed709 | ||
|
fb9a155e4c | ||
|
fbcc870d36 | ||
|
020e59743b | ||
|
0904de3f71 | ||
|
35f3430687 | ||
|
90e8cfc737 | ||
|
57bd3c3459 | ||
|
ad0d8fac91 | ||
|
a8b58d2007 | ||
|
a099ecf55b | ||
|
6b91b0477d | ||
|
fe2c63e8e4 | ||
|
a3febdf00e | ||
|
4ca174fa0b | ||
|
294582f136 | ||
|
d7e7c1f48c | ||
|
215800fb67 | ||
|
b05d428b2e | ||
|
d2adadb600 | ||
|
8e8c0b3fc5 | ||
|
65d63487f3 | ||
|
7c5932a512 | ||
|
610a3fcf83 | ||
|
b329dc41bc | ||
|
ff78606153 | ||
|
ef249066cd | ||
|
80188629df | ||
|
f77093731c | ||
|
ca7d58c272 | ||
|
08f3351a7a | ||
|
f499ada94c | ||
|
f073040c06 | ||
|
49bfb43bd2 | ||
|
425c70719c | ||
|
1420aea600 | ||
|
4543062700 | ||
|
722468a859 | ||
|
492a9ed3cf | ||
|
420b577606 | ||
|
434038fd12 | ||
|
a9b154b783 | ||
|
a01652b816 | ||
|
4440e82bb9 | ||
|
a2e1efab90 | ||
|
7e1ce38f0a | ||
|
a93bae616e | ||
|
a2568936a0 | ||
|
23408827b3 | ||
|
ae6e2fab6f | ||
|
3af36644c0 | ||
|
db8f627f1a | ||
|
5db0fa080b | ||
|
d81071f254 | ||
|
ba148dfd88 | ||
|
dbcb457a04 | ||
|
b00e1c8f47 | ||
|
899a6eb104 | ||
|
6e556c82a3 | ||
|
35ce64cc3a | ||
|
09b218d16c | ||
|
7ea451e027 | ||
|
ffb27c3946 | ||
|
6e7d2b50a0 | ||
|
bd025f30ff | ||
|
c6cbd8e826 | ||
|
b85afe3ca7 | ||
|
5a4674c973 | ||
|
769418e5ae | ||
|
38faed5799 | ||
|
10a578428b | ||
|
96cdf4a87e | ||
|
c0b1580561 | ||
|
28f9df7178 | ||
|
55a0c10855 | ||
|
7c5165763d | ||
|
d2836ebcb9 | ||
|
fecfac5ad9 | ||
|
5ed1ec8809 | ||
|
431cbf01af | ||
|
af792dfc06 | ||
|
d3b6c5f052 | ||
|
f48eb27f00 | ||
|
b02ac2561f | ||
|
1b2f66b201 | ||
|
e992f6b683 | ||
|
8b3f9db497 | ||
|
0eebfe4a8c | ||
|
12b3fc257b | ||
|
66a3322ea6 | ||
|
4965cb7121 | ||
|
b92b1fecb0 | ||
|
43a75420d0 | ||
|
e85026f866 | ||
|
ba7b9f3ad8 | ||
|
4eb58d3591 | ||
|
8d8e58a905 | ||
|
8f58da4f52 | ||
|
a4e2aedde1 | ||
|
24d04fe928 | ||
|
0cf32f6c5e | ||
|
84d850ee0b | ||
|
51930d3ccf | ||
|
c5e5316f2e | ||
|
829ab1eb37 | ||
|
d9b330b46a | ||
|
c032b92d0d | ||
|
ae92a6eed4 | ||
|
712c4463f7 | ||
|
fbdc1e1cdb | ||
|
f2060fe7a1 | ||
|
6950c6bf69 | ||
|
8a76c6c0de | ||
|
ef51cf5702 | ||
|
0c3988202e | ||
|
987446cf7f | ||
|
6dd44317c4 | ||
|
d2fc705846 | ||
|
e393150e9c | ||
|
43bfd28bcd | ||
|
ca8f8a59f4 | ||
|
85a26ae052 | ||
|
c30b1145a1 | ||
|
d086ade91f | ||
|
64d4c458ec | ||
|
2478450694 | ||
|
9f19a042e6 | ||
|
922ffcc0be | ||
|
f887fcafe3 | ||
|
48b562f71b | ||
|
5203742b05 | ||
|
f14b494bfc | ||
|
9a4ce57001 | ||
|
10af961fdf | ||
|
33cffe45fd | ||
|
a0ce75a978 | ||
|
8d168f6ad4 | ||
|
0875c5bf3b | ||
|
b17b28d9d6 | ||
|
e95249dc1b | ||
|
bb4bdfede5 | ||
|
e2b241d416 | ||
|
aeb94fb879 | ||
|
8e71cbd822 | ||
|
9896ec2ba6 | ||
|
9f9a74809e | ||
|
acce81cdcd | ||
|
d72953ae78 | ||
|
32213b1236 | ||
|
761d461bad | ||
|
774a3b3dcd | ||
|
30567d0e87 | ||
|
6b53eb5384 | ||
|
767ef62b64 | ||
|
f7e6d1e5c8 | ||
|
551686c2de | ||
|
b73c041cc3 | ||
|
9e8d85285f | ||
|
f41fbb3b3d | ||
|
3f5240157e | ||
|
7c807f754e | ||
|
9e1b23caf6 | ||
|
c2bad62d36 | ||
|
4516d44cc0 | ||
|
9c02b01318 | ||
|
08fab451d2 | ||
|
d47570c642 | ||
|
4547386b1f | ||
|
ab01dc5e54 | ||
|
380c742aad | ||
|
368359917b | ||
|
1c1b570cb3 | ||
|
3fe43372a7 | ||
|
c7bc1d7dcd | ||
|
a7ea6ec80d | ||
|
9cf574b9e5 | ||
|
20e63a1190 | ||
|
f6fc5de520 | ||
|
0b89118cd1 | ||
|
fa3a81575a | ||
|
6990766f75 | ||
|
e1e1a66a0b | ||
|
e552994f68 | ||
|
b33afd99a5 | ||
|
3c2ba4a0ea | ||
|
ab0fc07ee9 | ||
|
001650d48e | ||
|
659431ebfd | ||
|
0a419cb105 | ||
|
a4d4d17b82 | ||
|
0829ff6908 | ||
|
2d1b0fd6d0 | ||
|
122539596d | ||
|
86015e59a1 | ||
|
107a23f1ec | ||
|
abce068b97 | ||
|
28fd9241d4 | ||
|
3fc4746a52 | ||
|
1ea1e00344 | ||
|
598f219105 | ||
|
bbf7193c51 | ||
|
adecb21ada | ||
|
fa4679dcc4 | ||
|
58917eeeb4 | ||
|
f36e193650 | ||
|
dac484136f | ||
|
999ad857ae | ||
|
d073b9da65 | ||
|
aaa0b25106 | ||
|
f7dabcaeb0 | ||
|
3409086978 | ||
|
6c201db3dd | ||
|
da47df0a25 | ||
|
eea215dacf | ||
|
0949271dda | ||
|
6bb9227159 | ||
|
a95a37277c | ||
|
48b9aa5c18 | ||
|
1d7203a6fb | ||
|
1030b00f0a | ||
|
0c16d1fc11 | ||
|
ed04375385 | ||
|
6e49d20383 | ||
|
dc34eed203 | ||
|
fbe4f3fad8 | ||
|
e7ee3a06c7 | ||
|
599877195b | ||
|
7f9d267992 | ||
|
52904db413 | ||
|
5e0bde226a | ||
|
0695039ee3 | ||
|
a4bcd4451c | ||
|
e5b4b61832 | ||
|
dd55dc277b | ||
|
81ef1ae964 | ||
|
46701e4de7 | ||
|
0f52ccb433 | ||
|
8aed84e6af | ||
|
568497e1c5 | ||
|
f3f8e0013d | ||
|
d03c145189 | ||
|
682a5fde94 | ||
|
cca5bac30a | ||
|
64e102a8c6 | ||
|
f9972f50e0 | ||
|
05d9a7e820 | ||
|
df290950ea | ||
|
ae64be4802 | ||
|
348f3c382e | ||
|
d98678fc5f | ||
|
1da157d33f | ||
|
de4f004a08 | ||
|
522ef282c8 | ||
|
b5522ea260 | ||
|
b1f184c493 | ||
|
d66dd51bf6 | ||
|
0baed55a44 | ||
|
390b359761 | ||
|
311f41c610 | ||
|
0a156c76a2 | ||
|
70eaaa9e3b | ||
|
3e622f7185 | ||
|
3f12be50ac | ||
|
68412b49a1 | ||
|
c9b2620461 | ||
|
337b03aa68 | ||
|
df3f16ccf1 | ||
|
22aa5c41b5 | ||
|
8e09567221 | ||
|
3505788d42 | ||
|
91e0c9b940 | ||
|
00996b047f | ||
|
44fc0f367d | ||
|
b0e6259073 | ||
|
6255b2a907 | ||
|
a5fb0be274 | ||
|
e835f6d998 | ||
|
54ff564bb1 | ||
|
f8a5ded0ba | ||
|
a1be66f02b | ||
|
0815ad2cf0 | ||
|
3484047ad4 | ||
|
a63fa1cce5 | ||
|
59119ebc3b | ||
|
276f1b7e68 | ||
|
c482e5b5ca | ||
|
8c3e0adf35 | ||
|
64ff3ac672 | ||
|
cfe92525ed | ||
|
0e3a9ee2b2 | ||
|
a921db2cc6 | ||
|
3d99f86630 | ||
|
8d1a58ec06 | ||
|
2e5a7cb5a1 | ||
|
895f02f178 | ||
|
c59869982b | ||
|
3b3368f950 | ||
|
3f02c760c2 | ||
|
fee603e500 | ||
|
ad31d8014d | ||
|
58a0ac74d2 | ||
|
184984c16f | ||
|
2cf7a5e114 | ||
|
8388aa9c23 | ||
|
c1894d8c00 | ||
|
f7f9cdc518 | ||
|
850d7f8220 | ||
|
051043283c | ||
|
15bc69de01 | ||
|
ee3cfa4d6d | ||
|
df1f3079a5 | ||
|
d9ae8a5552 | ||
|
2326c31ee7 | ||
|
91cb0f30dd | ||
|
c0307c352c | ||
|
8fd7c1b313 | ||
|
b8147659b1 | ||
|
7a1bac682f | ||
|
9fdb7c977f | ||
|
4f3948323b | ||
|
70fcc1f712 | ||
|
f20fe9199f | ||
|
91dee4a3b8 | ||
|
0b89b8084e | ||
|
a5a80302b2 | ||
|
e61a24ee7b | ||
|
55ed342b59 | ||
|
3c6f79eec0 | ||
|
590800ac1d | ||
|
95c412b946 | ||
|
a232395750 | ||
|
6edbc8b6a5 | ||
|
f8ffeed302 | ||
|
e2ee68427c | ||
|
74ff23239d | ||
|
f1fa2ba2f6 | ||
|
e1522cec94 | ||
|
8841b3cbb1 | ||
|
94260bd93f | ||
|
15ff8af7ac | ||
|
d420033b36 | ||
|
bda63f0310 | ||
|
54add26ccb | ||
|
089b068362 | ||
|
fe474b4507 | ||
|
bbe15b563c | ||
|
59025b8f47 | ||
|
1b42c5edb1 | ||
|
362335913d | ||
|
4340dac595 | ||
|
f3e1fc884c | ||
|
39c06d8817 | ||
|
91cee36c21 | ||
|
6bef883942 | ||
|
25ba2406c0 | ||
|
e4dc8f85a7 | ||
|
12a4a260c8 | ||
|
268f02b5c3 | ||
|
13eff43b87 | ||
|
e604a19bce | ||
|
e63e39fe9a | ||
|
584c951824 | ||
|
f0d9982ee4 | ||
|
c65de74d13 | ||
|
df0a9701ba | ||
|
4ec7b1ff1e | ||
|
7d3a465386 | ||
|
30347900d9 | ||
|
e5f88fe2f4 | ||
|
0d0ccfd0ac | ||
|
9013d11d24 | ||
|
fc5672a161 | ||
|
221c3629e4 | ||
|
76fc56f1c9 | ||
|
8e59aa2885 | ||
|
0738dbd613 | ||
|
196ecffaf3 | ||
|
a0fedbd4b0 | ||
|
7c47e22000 | ||
|
6aad6a1618 | ||
|
b764172500 | ||
|
c185d79672 | ||
|
76b8ba91dd | ||
|
0418c831e6 | ||
|
4078f94caa | ||
|
a12ae8ad24 | ||
|
498ca29aab | ||
|
ba70e457b6 | ||
|
d62808fe1d | ||
|
6c14b79dfb | ||
|
631a253bcc | ||
|
4cb63100d3 | ||
|
42fcee0cfd | ||
|
829a2e937b | ||
|
5d7e5e8e59 | ||
|
6f0a0ef324 | ||
|
f7fe91abeb | ||
|
7252e8d160 | ||
|
2630c35f8c | ||
|
49f466c073 | ||
|
c198f785e6 | ||
|
5be093dafc | ||
|
2c33d5256c | ||
|
4448e2b5df | ||
|
146d234dec | ||
|
18d5c924e6 | ||
|
b520838195 | ||
|
1b036b763c | ||
|
8545a8bf0d | ||
|
f0136a5018 | ||
|
6697b3376b | ||
|
ea785f79b8 | ||
|
0352a09de7 | ||
|
5b4f15ab2e | ||
|
fd37c2b76b | ||
|
924aa05681 | ||
|
84b42210f1 | ||
|
941080c395 | ||
|
35d9a10cf4 | ||
|
7c181379b4 | ||
|
f9576d8afb | ||
|
6a8a113fa1 | ||
|
ef59c34165 | ||
|
a19e1f06c0 | ||
|
a9371f0a90 | ||
|
a7a94e49e8 | ||
|
affd100298 | ||
|
fd6ec301a4 | ||
|
5666e6084b | ||
|
69309c437e | ||
|
e392e4d344 | ||
|
bd53856927 | ||
|
cbd1018ecf | ||
|
46606152eb | ||
|
e6f93e0a08 | ||
|
8d81f1822f | ||
|
5903607363 | ||
|
590a8f52db | ||
|
ecac47d1bc | ||
|
3b477ef637 | ||
|
e2ef5b2ef3 | ||
|
1d59feeb72 | ||
|
c53dd31765 | ||
|
4c02081992 | ||
|
cb57af3c53 | ||
|
01d810fc00 | ||
|
8c2a9279ee | ||
|
0d65448f3d | ||
|
9da2b3c11a | ||
|
95400da977 | ||
|
dc41dc4c69 | ||
|
a5c11d4c23 | ||
|
878394535e | ||
|
35dba27a55 | ||
|
f22ad13fa9 | ||
|
aa2e5cb87b | ||
|
7740f3da7e | ||
|
badb576991 | ||
|
c65a63fc7e | ||
|
0111747016 | ||
|
eac4b0d87b | ||
|
3dadce4da4 | ||
|
1864468818 | ||
|
1a59379162 | ||
|
31d34c3946 | ||
|
3cc394f02d | ||
|
53c4fe9e80 | ||
|
d5521068b0 | ||
|
a63ef4010d | ||
|
cec3e86eef | ||
|
8950bb7af9 | ||
|
9e6fe7ceb9 | ||
|
c333d18cd0 | ||
|
0271ef69c9 | ||
|
2d493a4ea2 | ||
|
e339ab856f | ||
|
782904a971 | ||
|
a3753c01bc | ||
|
d5c3921846 | ||
|
a2c462b3da | ||
|
8673c7ef6e | ||
|
8d7be7757f | ||
|
6b83927048 | ||
|
e07adbd60e | ||
|
7798b8dcdc | ||
|
146e7970bf | ||
|
f4f7cc58e3 | ||
|
21b4b494e7 | ||
|
7307844bee | ||
|
5d419dd4ec | ||
|
6d0db7cc5e | ||
|
8de606588c | ||
|
5842b1272d | ||
|
35b0a85818 | ||
|
fcdd85af6c | ||
|
5aac2dc9df | ||
|
17a9b4e442 | ||
|
becb0b37e6 | ||
|
67ca876567 | ||
|
464ce66fd5 | ||
|
3e505481fe | ||
|
c90c3a183e | ||
|
d1a7e734dc | ||
|
6054982379 | ||
|
85b3278c8a | ||
|
c90c287601 | ||
|
6ee395ed12 | ||
|
6275ac2b81 | ||
|
fd0a6ec71f | ||
|
6c1c814aca | ||
|
43791f00aa | ||
|
538ac30b4e | ||
|
58f11489db | ||
|
acddf36467 | ||
|
166d32032a | ||
|
e4238a62c9 | ||
|
ad9c466712 | ||
|
a3d31bbaf1 | ||
|
4821139501 | ||
|
83213800b9 | ||
|
265ae19591 | ||
|
c1598d20b5 | ||
|
0712259057 | ||
|
ea42a5617f | ||
|
58a690e2c3 | ||
|
3ae2f0086e | ||
|
19c83cc54d | ||
|
8ac298e07d | ||
|
9b43e4ea3d | ||
|
dbacfb964b | ||
|
a664a6a790 | ||
|
ee1f072056 | ||
|
a6aabaa7f0 | ||
|
49b307db60 | ||
|
f7341cd9ab | ||
|
6932fb9935 | ||
|
2343e739d1 | ||
|
fc82f0b622 | ||
|
c0c50f2e18 | ||
|
9332d7207e | ||
|
a8c79b807b | ||
|
2637311ef5 | ||
|
06b5b8f793 | ||
|
61f58173cb | ||
|
b7b66f6cba | ||
|
dda2316884 | ||
|
b782679d1f | ||
|
b0f19f8f70 | ||
|
de5f31ac58 | ||
|
214f49e356 | ||
|
d7658ee9f9 | ||
|
70c864bc2f | ||
|
9804eccbf0 | ||
|
d1f24d45da | ||
|
9630625449 | ||
|
b72153f62b | ||
|
0a88a0c95e | ||
|
ab4ba9bb17 | ||
|
a49218a840 | ||
|
b6d633ab24 | ||
|
133943cd4e | ||
|
f8ffb1a179 | ||
|
41c4e0c83e | ||
|
99f6bb5ac6 | ||
|
3e0306f646 | ||
|
84e4f9a1c1 | ||
|
cd5ce6dd5e | ||
|
9ec4e24ef6 | ||
|
fa447ccded | ||
|
ef838627c4 | ||
|
b8aaf14cdc | ||
|
2740543abf | ||
|
3c526db52e | ||
|
cfe0414d96 | ||
|
08e06ba11a | ||
|
8c03164ea5 | ||
|
0fe2f226bc | ||
|
55b5702158 | ||
|
a4cbbb3868 | ||
|
816b01c1fc | ||
|
483e7549f8 | ||
|
60d71863dc | ||
|
170244e679 | ||
|
472e1da792 | ||
|
cbf03d58c8 | ||
|
ba41d84af9 | ||
|
98831a9449 | ||
|
9692539a3f | ||
|
76df332b57 | ||
|
c6405340bc | ||
|
775e3c065e | ||
|
8937b3ec86 | ||
|
3fbb86fded | ||
|
0cf2f7f254 | ||
|
9e571b87e8 | ||
|
23bafb6233 | ||
|
6dec65c5d9 | ||
|
4e59eb8958 | ||
|
756d5e685a | ||
|
f52530b848 | ||
|
c2bf37b878 | ||
|
98a2dd04b8 | ||
|
694ea689c8 | ||
|
618aaaf243 | ||
|
9224ffbf73 | ||
|
892612c084 | ||
|
077165b807 | ||
|
7994fc6407 | ||
|
d98df3e47d | ||
|
1064b1a08b | ||
|
0b7a7ed0f1 | ||
|
114b1aac76 | ||
|
6d06953a0e | ||
|
0430fc8a47 | ||
|
7338f5f985 | ||
|
640bcdd504 | ||
|
c9d5d996e5 | ||
|
710befec0c | ||
|
8ccb158241 | ||
|
97199d9b91 | ||
|
5a8b895475 | ||
|
6c9600cda0 | ||
|
82fa6a4fd8 | ||
|
45f2d7ab70 | ||
|
33731b969a | ||
|
40a8cdc71f | ||
|
cbe83987d8 | ||
|
01e4bf3a77 | ||
|
b198a8ea07 | ||
|
e2e87766fa | ||
|
f005a0975d | ||
|
5700369935 | ||
|
8a1fb6fe4e | ||
|
5b788dad2f | ||
|
fa2bd40d5f | ||
|
074bfadb28 | ||
|
bd60c793be | ||
|
90f3d2568a | ||
|
c73cdefe6f | ||
|
c5093168b1 | ||
|
a35e309a2f | ||
|
d4ff7bbe4d | ||
|
d4d73fc5fc | ||
|
bb35030112 | ||
|
7aed0354f1 | ||
|
c4f763960c | ||
|
c5182a4589 | ||
|
fc1a376fbd | ||
|
27387a134f | ||
|
be7bb2df9e | ||
|
72a291a54a | ||
|
b1342d84fb | ||
|
cdd57190ce | ||
|
d200a098cd | ||
|
a0ed3f53a4 | ||
|
e5c12b18af | ||
|
7808a1553e | ||
|
a0ba016171 | ||
|
344704b6bf | ||
|
3303e41a39 | ||
|
4e71ae0e59 | ||
|
9daf7a6668 | ||
|
a2b2b63932 | ||
|
af06774ba6 | ||
|
244d4f78e2 | ||
|
311fe98f44 | ||
|
6f7c8d96b9 | ||
|
ff6ec83b1c | ||
|
ea10eec926 | ||
|
be561a1609 | ||
|
6f724f648d | ||
|
048776e090 | ||
|
dedf65bd4b | ||
|
a7c02733ec | ||
|
59346db427 | ||
|
25efee55b8 | ||
|
a79ed02ccf | ||
|
79f87babdf | ||
|
f296d5138b | ||
|
b30445c5f8 | ||
|
d105613e51 | ||
|
ef43e78d54 | ||
|
6f61fbb127 | ||
|
9f9b7cab99 | ||
|
f129e16878 | ||
|
8a42dce763 | ||
|
6423d5e474 | ||
|
6e91157dcf | ||
|
85c61c1bc1 | ||
|
54af36fb85 | ||
|
fcdcc939e6 | ||
|
13450d5afa | ||
|
5e1e653095 | ||
|
e8fabcb449 | ||
|
a4ce41ed39 | ||
|
1b42062d57 | ||
|
c2a4b01a9c | ||
|
47e763b0cf | ||
|
0278f6c9f2 | ||
|
d96bc14516 | ||
|
318f433f22 | ||
|
cfc80cb9b0 | ||
|
01c6149422 | ||
|
6f80a6c08a | ||
|
8fb2d38cd1 | ||
|
5018d27c25 | ||
|
1d77101253 | ||
|
1ddd468c1f | ||
|
f05cd9ea51 | ||
|
70c00a4150 | ||
|
d296029e8e | ||
|
e257fd8628 | ||
|
119c1b43be | ||
|
1277ce38de | ||
|
6761b91400 | ||
|
2a6244a5c2 | ||
|
777bddd3d8 | ||
|
e2b13791bb | ||
|
f44c21ce59 | ||
|
ade977e416 | ||
|
f09a131bd6 | ||
|
4815587de1 | ||
|
e0ebfb9b53 | ||
|
90836afd91 | ||
|
4e1b0a25bb | ||
|
89c3236bf5 | ||
|
7658bc2025 | ||
|
7cf60c7c35 | ||
|
ccde725d3b | ||
|
e3b45cac0a | ||
|
8f8a500dcd | ||
|
f9749cd82c | ||
|
051052fdd2 | ||
|
940304b4c2 | ||
|
b4d2fae27f | ||
|
11e194292c | ||
|
5ba6f6f53e | ||
|
f58a16905f | ||
|
33e82b336b | ||
|
0ced712974 | ||
|
db8e35cc13 | ||
|
b6db5aa2d3 | ||
|
396dc5c9b0 | ||
|
67e424a32a | ||
|
d8cbec41d2 | ||
|
374f6b8d52 | ||
|
20ec4cbd14 | ||
|
1c80835f49 | ||
|
5e0af26c27 | ||
|
b42674ac06 | ||
|
3394543705 | ||
|
75c51aa61b | ||
|
6041722250 | ||
|
60d038b367 | ||
|
b2c4bf96af | ||
|
f007f9a86d | ||
|
b1c1634950 | ||
|
5157ccf7c0 | ||
|
c4a782301d | ||
|
17fe94fa46 | ||
|
75d9347d23 | ||
|
ef784124f3 | ||
|
bd1b631914 | ||
|
edfc8cfdc4 | ||
|
fbe34015d4 | ||
|
391fa008d0 | ||
|
7df8381b8f | ||
|
c0234ae328 | ||
|
5c64a85d7c | ||
|
7aa8f115ce | ||
|
cf2c8d6c67 | ||
|
37edebcad9 | ||
|
4d4f661548 | ||
|
46e4cb4f50 | ||
|
34e622cf0c | ||
|
7ccb99aa2c | ||
|
9e3847e56f | ||
|
90ced351f4 | ||
|
04295ea8c5 | ||
|
2452d3c24b | ||
|
302428f1d1 | ||
|
cf603aa80e | ||
|
1f9f949a8c | ||
|
0bde1e97dc | ||
|
42aca2e40f | ||
|
e1e44d35bb | ||
|
a790b2e529 | ||
|
357946388c | ||
|
b774583f28 | ||
|
6436daca08 | ||
|
f153c7bb80 | ||
|
1f8a618dcc | ||
|
2d853e5a2f | ||
|
361dc79ede | ||
|
19173321ea | ||
|
87b724ec72 | ||
|
67db0f950b | ||
|
f85bbf12ca | ||
|
37e4f1e8d5 | ||
|
44d8bfd763 | ||
|
0cdbad6194 | ||
|
4799a8a68e | ||
|
8caec81d1e | ||
|
83d5bf45e5 | ||
|
037eb8a163 | ||
|
3e5025b46e | ||
|
35a5dc6219 | ||
|
ace3ca0ad9 | ||
|
a8a498ddea | ||
|
d16663f0a9 | ||
|
623470209f | ||
|
553de5a873 | ||
|
ccf8773b18 | ||
|
cad25bf85d | ||
|
9ce748452d | ||
|
9263d74b75 | ||
|
9601bddc84 | ||
|
e281e19052 | ||
|
0238b78f45 | ||
|
0ccee4326d | ||
|
e9ab54f657 | ||
|
0afb1a2d04 | ||
|
2d2b2964a5 | ||
|
10c4f7631b | ||
|
d921cffdaa | ||
|
5369e21780 | ||
|
d34fb4494e | ||
|
1bd493ea37 | ||
|
391c3fe4c9 | ||
|
3a7da9f13b | ||
|
3780767ccc | ||
|
411279b3eb | ||
|
be3069e0e5 | ||
|
22cf870555 | ||
|
d6479c1390 | ||
|
a4e82c79cc | ||
|
bcc89adb5f | ||
|
a41c9e339a | ||
|
feeca77436 | ||
|
fcce12ba40 | ||
|
f4b06fb834 | ||
|
e7fd803d19 | ||
|
3b96747871 | ||
|
33088df07d | ||
|
a215714b6b | ||
|
6a9904fd43 | ||
|
cc297ccfcd | ||
|
c7c88dec04 | ||
|
e481a5926a | ||
|
0464245218 | ||
|
0001f31c06 | ||
|
391d31759a | ||
|
ed2f2435d2 | ||
|
6e6b2ccfa0 | ||
|
be9a73560d | ||
|
e82b5a4ecf | ||
|
a27d8ac828 | ||
|
6267258189 | ||
|
e7527f532e | ||
|
8b6e74d505 | ||
|
e6106c0c4e | ||
|
f52bafc014 | ||
|
9e0630ea79 | ||
|
968bfb92d0 | ||
|
284c9fcee2 | ||
|
5b0b939531 | ||
|
2efac0c96b | ||
|
dc52fd1dcf | ||
|
19240a9caf | ||
|
4eef28f93d | ||
|
c6e5c4e3b5 | ||
|
007f567c7a | ||
|
ffe178c64c | ||
|
c3835cefb1 | ||
|
2c382f3d3f | ||
|
b592aa6a02 | ||
|
57e82b62a1 | ||
|
13f3aca838 | ||
|
94b17eaff3 | ||
|
eb9b94b9c6 | ||
|
a3038da3d7 | ||
|
6026b7800a | ||
|
36c5b71656 | ||
|
a320bea68a | ||
|
a87fe8b44d | ||
|
0a2b4dedc7 | ||
|
f7ed3eefc8 | ||
|
8bb3a3f8a6 | ||
|
89d08ca359 | ||
|
b80aec37e0 | ||
|
e34fd855a9 | ||
|
fc12ea18b8 | ||
|
f87df53791 | ||
|
d6746362a4 | ||
|
2850bd0b46 | ||
|
b762eff4eb | ||
|
4b3b96447f | ||
|
13bcfbe3c5 | ||
|
8525fb89f8 | ||
|
ed2d1c4932 | ||
|
5091f8457e | ||
|
84b69fc58c | ||
|
a2cac003a4 | ||
|
7c16a90221 | ||
|
97cdd53861 | ||
|
b7ee00fb22 | ||
|
ef2ee20820 | ||
|
4866e5050a | ||
|
8e36c456e1 | ||
|
4b8bcd265b | ||
|
0db681eeda | ||
|
8823f69256 | ||
|
f3e9dfe734 | ||
|
a7b31ab1f9 | ||
|
644ab27186 | ||
|
e90ecd2085 | ||
|
bc38184ebf | ||
|
d9de27e6f2 | ||
|
6930bf0200 | ||
|
199833bdd4 | ||
|
0dcd2e6e93 | ||
|
0dd43d5c9a | ||
|
e879bd0fc5 | ||
|
8bf7daff65 | ||
|
ae0f01d326 | ||
|
af8d0a3965 | ||
|
1b170c74c0 | ||
|
f6b9ff50c3 | ||
|
9ef75ebcde | ||
|
f76a618768 | ||
|
098d7baa4d | ||
|
59a57d3d28 | ||
|
cce95e09de | ||
|
ec48ebcd79 | ||
|
908f80a15d | ||
|
02eab89d82 | ||
|
c588786a06 | ||
|
b4f3105035 | ||
|
d018f11877 | ||
|
d0000c6131 | ||
|
c05ffefd7d | ||
|
530fc67a05 | ||
|
c79ec11b07 | ||
|
668ef26056 | ||
|
75ec7723ef | ||
|
73e609fa29 | ||
|
8cb06bf451 | ||
|
1be8a059f4 | ||
|
7f41c7ab0e | ||
|
3860c5d8ec | ||
|
a061a7cc4d | ||
|
844501d6cd | ||
|
020bd00b8f | ||
|
0706b0d287 | ||
|
ce56cd2b16 | ||
|
b7a0a7eea4 | ||
|
824d14e793 | ||
|
83e0ed2b5d | ||
|
c8b70b51c3 | ||
|
c0fedaa3a4 | ||
|
e74dcff010 | ||
|
3b5b45b463 | ||
|
fead482b0d | ||
|
29bd8203b5 | ||
|
08b79e45cf | ||
|
3a05a0bcaa | ||
|
d0aafaee60 | ||
|
332b874493 | ||
|
6c995ed738 | ||
|
fb09d77cdc | ||
|
9c952785e6 | ||
|
2f51c1bf47 | ||
|
276a110e90 | ||
|
b761674b2c | ||
|
0b20bf0145 | ||
|
1397cbeac2 | ||
|
06e122f303 | ||
|
f062acfd7c | ||
|
97ca414fc0 | ||
|
a9af5bcec4 | ||
|
7e30bf4197 | ||
|
c724ea9f69 | ||
|
e6cc4a1180 | ||
|
3cce097b9d | ||
|
53f9d6869d | ||
|
61beee0f49 | ||
|
1f3d400ad6 | ||
|
f2ff2187d9 | ||
|
28ddc0055f | ||
|
90b5b6bd8b | ||
|
53466797a5 | ||
|
f5235938b7 | ||
|
054860b38d | ||
|
b60d02b8f4 | ||
|
0d69797851 | ||
|
bfffff0750 | ||
|
b7bcd8da7d | ||
|
d3862c97ba | ||
|
c069c39ce1 | ||
|
e994d501b0 | ||
|
caf163f98c | ||
|
1c408db907 | ||
|
8d44e07c32 | ||
|
d99fae4340 | ||
|
d49caa29ce | ||
|
8bebe11b4e | ||
|
236a456cae | ||
|
7bc745fa8e | ||
|
056fc8fbaf | ||
|
b6aa507b41 | ||
|
4b1a5a5e14 | ||
|
a364206159 | ||
|
b5feb5f733 | ||
|
991125034e | ||
|
a0fe1a85f1 | ||
|
3a2e68c334 | ||
|
b6418cd912 | ||
|
e652038018 | ||
|
b2e2af51ed | ||
|
9502444bbc | ||
|
a0fe803c35 | ||
|
ea2ca37abe | ||
|
0601ffbb34 | ||
|
09a7fcaba4 | ||
|
ce15784851 | ||
|
b861b2dffb | ||
|
e50fd786da | ||
|
5e82de667e | ||
|
d7ddcda9da | ||
|
6d031130b9 | ||
|
a61b0685f0 | ||
|
abfeafc823 | ||
|
3a51be3430 | ||
|
3b914d4a7f | ||
|
ede4802ceb | ||
|
fe79119809 | ||
|
319d96f94e | ||
|
6f07dc7852 | ||
|
16bcba6e2e | ||
|
1002acb021 | ||
|
b771544c5d | ||
|
8c7f09c454 | ||
|
618cffefb1 | ||
|
8fd37e857e | ||
|
8218bfd24b | ||
|
cbb2dbffb9 | ||
|
528a838643 | ||
|
cbed6418e7 | ||
|
4882cc92a8 | ||
|
28fb11068e | ||
|
394ced9fb9 | ||
|
90465149e6 | ||
|
c6d868d981 | ||
|
bada4fd140 | ||
|
60f96d15bd | ||
|
0328007345 | ||
|
3ad0e92a0f | ||
|
3934120541 | ||
|
24ca126f5a | ||
|
651ca71126 | ||
|
e7cb33d8e2 | ||
|
c63d238316 | ||
|
dcdc48d917 | ||
|
f4c1671079 | ||
|
7aa2fbee1c | ||
|
f1939fdc2b | ||
|
c9356d0ff5 | ||
|
6b5d938a40 | ||
|
d82da160f3 | ||
|
54c8aac20d | ||
|
314b6fc2f8 | ||
|
974df031a0 | ||
|
36d0292c6b | ||
|
7c16952c92 | ||
|
557807e3ba | ||
|
c1d5b2df29 | ||
|
05be5c1199 | ||
|
f19a65148a | ||
|
a55fc4fff9 | ||
|
35a7a70b93 | ||
|
3e0574e563 | ||
|
69e557e70d | ||
|
1b846be5fc | ||
|
707eb58068 | ||
|
8630f3be96 | ||
|
c90aeba286 | ||
|
5055cfc6cb | ||
|
c222c4eb29 | ||
|
6c01955561 | ||
|
305e0f1772 | ||
|
52a94e3256 | ||
|
a418fb18b6 | ||
|
9cd579d3db | ||
|
e1a6ba7377 | ||
|
04aabe0921 | ||
|
8dd4d71d75 | ||
|
49dd63af1e | ||
|
18c6f0ccc3 | ||
|
55c50a4b5b | ||
|
12b3267d5c | ||
|
d6d564c027 | ||
|
1fbd5f7922 | ||
|
f0e13fa492 | ||
|
c8d5ac9248 | ||
|
aa4f77d4de | ||
|
f3ef112297 | ||
|
bbb71083ef | ||
|
e2134d76ec | ||
|
651372cd64 | ||
|
581fe17b58 | ||
|
af8608f302 | ||
|
290a19b6c6 | ||
|
73eaa93be8 | ||
|
7ab17383a6 | ||
|
b103c5b13f | ||
|
b7d8a83017 | ||
|
e7bf4f455d | ||
|
a7f212c4f2 | ||
|
eb991c6026 | ||
|
a78af8f248 | ||
|
f34bdf0f58 | ||
|
ba272253a5 | ||
|
9f488b7b77 | ||
|
3fb7df18a0 | ||
|
00401080e0 | ||
|
b265dc3bfb | ||
|
63cabbe960 | ||
|
f6c1a7e6db | ||
|
a3dcacade9 | ||
|
17e65e422c | ||
|
f53e2ffa47 | ||
|
a1e4047695 | ||
|
47ce884bbe | ||
|
1b17c2613d | ||
|
dedc8d89c7 | ||
|
d00fce86d2 | ||
|
abab2d1cde | ||
|
33b715eb4e | ||
|
f6effbb6bb | ||
|
dff9ec0704 | ||
|
bfaf4c58e4 | ||
|
ab7d24b637 | ||
|
c256dae736 | ||
|
5a55ef64c4 | ||
|
045026431b | ||
|
4dff91a0e5 | ||
|
7105872a37 | ||
|
179bd8e018 | ||
|
c82293342f | ||
|
81bf79e9d3 | ||
|
8d6dffb3ff | ||
|
2f6a8f793b | ||
|
9bcd0bbfac | ||
|
cd359de7eb | ||
|
000f9ed459 | ||
|
c5b2c0b4ec | ||
|
b7e9af100a | ||
|
0d6409098f | ||
|
e07238ded8 | ||
|
27903f278d | ||
|
ddf966d08c | ||
|
65dca36ae1 | ||
|
289dae0780 | ||
|
71f802ef35 | ||
|
0135f25b9d | ||
|
de3ebf664f | ||
|
850d879726 | ||
|
5397e6c723 | ||
|
889f6fc5fc | ||
|
41c2ed7c67 | ||
|
cdf47d4719 | ||
|
210368bea0 | ||
|
4f48751d0b | ||
|
b6d3e82304 | ||
|
3bb3528aa5 | ||
|
4f892835b8 | ||
|
ac49221639 | ||
|
75ed5db3e4 | ||
|
59c8e4e6a2 | ||
|
52b322b756 | ||
|
dc876d9c96 | ||
|
5b028428c4 | ||
|
f67a0469fc | ||
|
494cd0efff | ||
|
fc8e38e862 | ||
|
f09fb5a209 | ||
|
b00c1c1575 | ||
|
7e5dd62a92 | ||
|
35718f6215 | ||
|
a6d3891a95 | ||
|
9591c4eb58 | ||
|
8aaf720b0b | ||
|
63a35c97bc | ||
|
8eddaa3fac | ||
|
1b3f37eb78 | ||
|
1f8fbfaa24 | ||
|
ea92ccb4c1 | ||
|
d25a77b633 | ||
|
51bb776e56 | ||
|
47b1b6daba | ||
|
adeb654248 | ||
|
c4d7335fdd | ||
|
ca7f42c409 | ||
|
ca02cd72ae | ||
|
1ba542eb50 | ||
|
53cd967541 | ||
|
49749a0bc7 | ||
|
446f924380 | ||
|
5b231170cd | ||
|
7375357b11 | ||
|
347d799d85 | ||
|
0d17f02191 | ||
|
ce5bc80347 | ||
|
0a4479fe9e | ||
|
de8e96cd75 | ||
|
e2a62f88a6 | ||
|
8926d2a73c | ||
|
114833cf8e | ||
|
32227436e0 | ||
|
28ff5636af | ||
|
656896d16f | ||
|
19bf8afece | ||
|
841b6c4ddf | ||
|
4c171df848 | ||
|
1f79d88840 | ||
|
6ee7e9d731 | ||
|
4856223838 | ||
|
74ea2a847d | ||
|
9813dde3d9 | ||
|
fea7b62b9c | ||
|
37e03bf2bb | ||
|
5656de79a2 | ||
|
70c6048cc1 | ||
|
87595fd704 | ||
|
dc030a42bb | ||
|
89283ed179 | ||
|
64e8a05a9f | ||
|
676320586a | ||
|
734fa51806 | ||
|
f056ecc8d8 | ||
|
1a722c1517 | ||
|
44607ba6a4 | ||
|
01d66212da | ||
|
925e10b19b | ||
|
1b4c75a76e | ||
|
3400e36ac4 | ||
|
78e2ae4f36 | ||
|
957944f6a5 | ||
|
9eab500e2c | ||
|
573f4675a1 | ||
|
e6bde3e1f4 | ||
|
5869174021 | ||
|
449761b6ca | ||
|
39d5ce19e2 | ||
|
3b156bc5c9 | ||
|
a4f5124b61 | ||
|
47a34c2f54 | ||
|
8a7446fb40 | ||
|
705e7d1cf1 | ||
|
44a90b4e12 | ||
|
54e5a65cf0 | ||
|
06a2c380bd | ||
|
33ac1fed2a | ||
|
cc65a7cd11 | ||
|
d600a54034 | ||
|
ba06225b01 | ||
|
ce60ab8e00 | ||
|
14f6fd19ef | ||
|
1d8351f921 | ||
|
6a55b052f5 | ||
|
2a36b83dea | ||
|
14acc4feb9 | ||
|
0657ca2969 | ||
|
e90c3a78d1 | ||
|
63c9bc5c1c | ||
|
a6bbc81962 | ||
|
b800fb5846 | ||
|
172a629da3 | ||
|
6d1f7b36a7 | ||
|
673ee4aeed | ||
|
25b787f6f2 | ||
|
6b74ef77e6 | ||
|
278201e87c | ||
|
703cdfe174 | ||
|
02988989ad | ||
|
25c17d3704 | ||
|
9b4d832d17 | ||
|
52ab19dec6 | ||
|
9973fe4326 | ||
|
2479f2d65d | ||
|
9056cb7026 | ||
|
cd9d9b31ef | ||
|
ff841c28e3 | ||
|
ca1379d9f8 | ||
|
5127f94423 | ||
|
f5910ab950 | ||
|
22efaccd4a | ||
|
c8466a2e7a | ||
|
209a9fa8c3 | ||
|
bc1af12655 | ||
|
e7e4cb7579 | ||
|
1b39db664c | ||
|
7397b9fa87 | ||
|
5bed5fb8fd | ||
|
fd795b4361 | ||
|
b2c0915a71 | ||
|
095083bcfb | ||
|
4ba72f7eeb | ||
|
6cb39795a9 | ||
|
00ba16f536 | ||
|
988a839623 | ||
|
8fa61e628c | ||
|
8f3620e07b | ||
|
190f70f332 | ||
|
6730683919 | ||
|
51b12567e8 | ||
|
d01cdeded8 | ||
|
d22a03f1a5 | ||
|
ab0aeec434 | ||
|
47ff51ce4e | ||
|
1d62ef357d | ||
|
59ef66f46d | ||
|
77479215a6 | ||
|
11da1f72b1 | ||
|
6375b9d14d | ||
|
42bc12f56d | ||
|
8515112811 | ||
|
bedb87674b | ||
|
029c038a49 | ||
|
95e905a5ae | ||
|
8a8ec32f2c | ||
|
cd958398af | ||
|
2eedafd506 | ||
|
9a88c0d579 | ||
|
cb6fc466d1 | ||
|
a6154cbb43 | ||
|
9c20967d24 | ||
|
79bbadad2f | ||
|
c3b44cee94 | ||
|
1968496ce1 | ||
|
e1feb46de9 | ||
|
f51a082049 | ||
|
0f1927b4b1 | ||
|
9baccc0784 | ||
|
95c9cc7f99 | ||
|
0ed8e04233 | ||
|
b46c328811 | ||
|
c7d88ed95b | ||
|
94da1a30dc | ||
|
219a5db60c | ||
|
a5cfedcae9 | ||
|
9b8a632f37 | ||
|
be0426d9a2 | ||
|
8fac0a02e5 | ||
|
fa696a2901 | ||
|
f5615b1c54 | ||
|
9850c294d1 | ||
|
8929f32068 | ||
|
3019bb5c97 | ||
|
95044d27ce | ||
|
16ac92b2ef | ||
|
f095964f7b | ||
|
cfba793fcf | ||
|
c50a11e58a | ||
|
a7282a5794 | ||
|
efaf313422 | ||
|
d9f5753f58 | ||
|
179318f1d8 | ||
|
dd33d24346 | ||
|
fa684eabab | ||
|
4b5bd6eed7 | ||
|
63f5946527 | ||
|
8dac3ebf96 | ||
|
09e783fbf6 | ||
|
b18b686545 | ||
|
e99ea41117 | ||
|
172a37ec8c | ||
|
da6b341b63 | ||
|
16d93b1775 | ||
|
e15cf063c6 | ||
|
5ac9e3e47a | ||
|
743a61bf49 | ||
|
c790ea07dd | ||
|
b4f980b349 | ||
|
673f23b6a0 | ||
|
8c325f3a8a | ||
|
f71516f36f | ||
|
1752386a6c | ||
|
112675c782 | ||
|
3b6ba7641d | ||
|
477a35a685 | ||
|
2a0a39a95a | ||
|
dfc77db51d | ||
|
c9596fd8c4 | ||
|
78373f3746 | ||
|
ebc3db8aed | ||
|
756601c1ce | ||
|
8bb5077e76 | ||
|
5b85f01427 | ||
|
a7d594e566 | ||
|
481e6671c2 | ||
![]() |
b890e02a6a | ||
![]() |
4772840b4c | ||
|
cd7adc997b | ||
|
9fdc901b7a | ||
![]() |
76ec3473d6 | ||
|
b29ae67501 | ||
|
24f1fb566d | ||
|
a253294890 | ||
![]() |
0b53de1bb6 | ||
![]() |
746c68c9f6 | ||
|
ec008b4a16 | ||
|
1d65e34fe5 | ||
|
8ae78703ca | ||
|
bd4b9a9fd3 | ||
|
f09677d376 | ||
|
f669e3f6c4 | ||
|
961c5ea962 | ||
|
a1c61a1146 | ||
|
797dff4752 | ||
|
711ad638b8 | ||
|
4956c3328c | ||
|
96a82381d1 | ||
|
68190c8c76 | ||
|
dde47bc1fc | ||
|
744deaed8d | ||
|
a62910c8b6 | ||
|
e259a09b89 | ||
|
6472d8438a | ||
|
dc8a402a4a | ||
|
f72374488e | ||
|
3c6d9a4b8e | ||
|
3073388403 | ||
|
67f856c997 | ||
|
742fd0b444 | ||
|
e1d69c0eae | ||
|
49d4190615 | ||
|
64d39765ca | ||
|
aca8f64415 | ||
|
855b600268 | ||
|
476d613e20 | ||
|
fb8a4d0a41 | ||
|
130f3f6d41 | ||
|
ed997af043 | ||
|
3c47f6917a | ||
|
e32a486493 | ||
|
208da935a1 | ||
|
1dda9a875e | ||
|
b26175b7c6 | ||
|
ffc6befb38 | ||
|
9df7c96d08 | ||
|
32fa66f0a2 | ||
|
7153ed022c | ||
|
50e4e71abb | ||
|
d6e65a3d63 | ||
|
7a77b12834 | ||
|
fe387931a4 | ||
|
dc83e32173 | ||
|
f5b29bad33 | ||
|
f599aa4852 | ||
|
6f05de2fcc | ||
|
02016c77f1 | ||
|
93eede7c6b | ||
|
944e396823 | ||
|
8a654834ac | ||
|
bb76fa80cd | ||
|
53f44a4029 | ||
|
182dcc7e5f | ||
|
2d272e0207 | ||
|
9384a50879 | ||
|
00d4f35f2c | ||
|
483557163e | ||
|
2679252b04 | ||
|
5a95c827b4 | ||
|
f938d8c878 | ||
|
bfc0122c1b | ||
|
79691541ae | ||
|
05d0542875 | ||
|
1d22b39a1d | ||
|
549b68cf71 | ||
|
55f87de2e0 | ||
|
b8424e20aa | ||
|
bbe3a30e77 | ||
|
cdc5348a06 | ||
|
e64a3978e6 | ||
|
f2feb12708 | ||
|
5319c5f832 | ||
|
7531fe14fe | ||
|
0086c6373b | ||
|
10dc19652e | ||
|
2f2c4964c5 | ||
|
cb4203b6db | ||
|
bb5b62466e | ||
|
6407b5452b | ||
|
a742fecf9c | ||
|
60415b9222 | ||
|
ffb39ef074 | ||
|
d73f5ed2b5 | ||
|
7af973b60d | ||
|
49eddc9da5 | ||
|
4b1de108d1 | ||
|
e46c735efa | ||
|
56da7deb4c | ||
|
5f4649ee2b | ||
|
7cc2c4f621 | ||
|
cc449f9304 | ||
|
28425efbe7 | ||
|
8c3a22aa5c | ||
|
f3e5933599 | ||
|
39d8750ef9 | ||
|
52b0c244a8 | ||
|
ee95a00ce2 | ||
|
11566ec923 | ||
|
a78ff08202 | ||
|
ceb3969c8b | ||
|
6f048ef40e | ||
|
aac4fe37e8 | ||
|
ebfa941a4f | ||
|
47c70eea9e | ||
|
fe7c40cb7c | ||
|
8973626a4b | ||
|
ace5d999e2 | ||
|
52829a244b | ||
|
71c247fe56 | ||
|
b34066f579 | ||
|
b286c15c51 | ||
|
aff4f6c859 | ||
|
1f8f73fa30 | ||
|
620d6624a9 | ||
|
287f42ae64 | ||
|
d976c97d13 | ||
|
6d549b0754 | ||
|
02dd64558f | ||
|
ea5e2f5580 | ||
|
b65c7a75fe | ||
|
39f5dce51c | ||
|
f77ec1faf6 | ||
|
cd3a1e0159 | ||
|
3f20fadb81 | ||
|
1c6428dd3b | ||
|
aca620241a | ||
|
808b36e088 | ||
|
1613375cc0 | ||
|
787ef957d2 | ||
|
b915d0aed5 | ||
|
16dc5b5327 | ||
|
d25e98d9c4 | ||
|
227cdc1ec8 | ||
|
c2d0c63db0 | ||
|
f5c9807a48 | ||
|
7e9b1d5e16 | ||
|
5070c4eea9 | ||
|
33d9c51b6f | ||
|
d0f9d478c2 | ||
|
f296ec46c8 | ||
|
64d19e480b | ||
|
2c585a9328 | ||
|
45b0d9e19b | ||
|
026a4b896c | ||
|
78237fcd11 | ||
|
73cc3dcb92 | ||
|
4cff03e7fe | ||
|
8e35f131d5 | ||
|
aafb9d7e4f | ||
|
652f30bdbd | ||
|
f4ba7b2a0c | ||
|
592490d709 | ||
|
5ac676d201 | ||
|
abfe0a925a | ||
|
fa11f4f45b | ||
|
1e8dbfe4b7 | ||
|
f82f2bd287 | ||
|
76742c3869 | ||
|
9407e7e418 | ||
|
120552b36e | ||
|
9fb58c7ae3 | ||
|
b917da765c | ||
|
cc59510cd0 | ||
|
86aab7023c | ||
|
e3b0a41ba9 | ||
|
802c55dfc8 | ||
|
280f7814aa | ||
|
3174681bd8 | ||
|
853f80b928 | ||
|
8bdad377d7 | ||
|
9e1c2a5bd1 | ||
|
b1777b6011 | ||
|
904be3005f | ||
|
95eaf4c887 | ||
|
e3923b7b22 | ||
|
a43993e3ec | ||
|
bc6b3fb5f4 | ||
|
df47f5d47b | ||
|
319e64bacc | ||
|
ecf044bed1 | ||
|
76e1de38e8 | ||
|
1648a74ed7 | ||
|
c63a7884cb | ||
|
cffbd41f26 | ||
|
c443187d0b | ||
|
8c305d8390 | ||
|
0345c5c03b | ||
|
cc6ac4c9d9 | ||
|
2ceba45782 | ||
|
ed423ed041 | ||
|
f58a52eaa4 | ||
|
688404011b | ||
|
8881e0fb75 | ||
|
61de7e144e | ||
|
815934ff5c | ||
|
c3ff9e49e8 | ||
|
d52875aa8f | ||
|
9027cd290c | ||
|
58a7203ede | ||
|
5a84016a91 | ||
|
bb0269f484 | ||
|
1adc9349fc | ||
|
06215c83f2 | ||
|
8a828137ee | ||
|
de4b1c8f09 | ||
|
265d40f04a | ||
|
b64e52c0c0 | ||
|
ac02e5c0a6 | ||
|
427a415fbf | ||
|
9a3414aaa7 | ||
|
7f5486dade | ||
|
c8897ecf9b | ||
|
2c8b94d469 | ||
|
36c1cfae51 | ||
|
41ad78750e | ||
|
3eaa4d5b38 | ||
|
35176f9550 | ||
|
eb2c7268ea | ||
|
80311355ae | ||
|
39d1590ace | ||
|
0b36b650a4 | ||
|
39575e8542 | ||
|
326ef498b0 | ||
|
5148bad82e | ||
|
518f02472f | ||
|
ee5a132eb2 | ||
|
654dc5bff3 | ||
|
27aeb4f05f | ||
|
13dcf7f72a | ||
|
65c26f17df | ||
|
3bedba71d5 | ||
|
1ba64d9745 | ||
|
84bf570243 | ||
|
28d50bccf9 | ||
|
66711c2e9d | ||
|
92d8c37d7d | ||
|
5824f75669 | ||
|
deb8adafc9 | ||
|
d2649b237c | ||
|
255233fe38 | ||
|
6532c258f6 | ||
|
4ac3984b7c | ||
|
83e2b10904 | ||
|
26c1793d85 | ||
|
23a9eea26b | ||
|
af9b536dd9 | ||
|
e4874f86f9 | ||
|
e300a957e4 | ||
|
1c38afcd25 | ||
|
a06faa7685 | ||
|
019ab2b21d | ||
|
f6ba5f5d51 | ||
|
c4cbb64643 | ||
|
8260cec713 | ||
|
428af3c0e8 | ||
|
68544715bf | ||
|
d2ea5633fb | ||
|
f4520e2752 | ||
|
475802afbc | ||
|
3aa9b5f0b6 | ||
|
6c5dbf7bd0 | ||
|
3b3dc5032b | ||
|
a170668d9d | ||
|
f8dac39076 | ||
|
fe4ae61552 | ||
|
0c3597f757 | ||
|
6109bdeafe | ||
|
6e9a61c4e5 | ||
|
8e244fd956 | ||
|
2eb6771963 | ||
|
db77108054 | ||
|
241e2bef85 | ||
|
fac02dbc7d | ||
|
9ebcd55ff5 | ||
|
50244c1c40 | ||
|
b4395fdad1 | ||
|
1da8994be7 | ||
|
55ff1e2bb1 | ||
|
5fd8528c49 | ||
|
26d8ed783a | ||
|
c0c5bf1591 | ||
|
c17a481b74 | ||
|
a9a0e69ec0 | ||
|
ea1fed2fd3 | ||
|
b37f2c7d7f | ||
|
0c0c5ff077 | ||
|
e12b99d17e | ||
|
d599146c3a | ||
|
476731a2c3 | ||
|
1e491dd8fb | ||
|
ba6397b963 | ||
|
3146da6aec | ||
|
5643e57ede | ||
|
f532dbe7b4 | ||
|
ec2af62b4d | ||
|
423142d730 | ||
|
bdddb526da | ||
|
dbf1ed40b3 | ||
|
02ace06526 | ||
|
2d2bfc0a4c | ||
|
3c22a12cbb | ||
|
3071ef2f36 | ||
|
3022cb22d6 | ||
|
e9b4a3f6b3 | ||
|
4312ebfcc3 | ||
|
2c0e099d1c | ||
|
b1eb02eb1d | ||
|
c919797553 | ||
|
08dacab05c | ||
|
2efc9218df | ||
|
41505dae11 | ||
|
45efe7cd56 | ||
|
78cac7f0e6 | ||
|
a1a1b8e94a | ||
|
641a658059 | ||
|
44ec447014 | ||
|
98308ecf98 | ||
|
8d613a6472 | ||
|
c3e5298ecd | ||
|
e89d31eb5a | ||
|
08f3d653cc | ||
|
f2bbafe6c2 | ||
|
cb80280eaf | ||
|
f22f954ae3 | ||
|
2556855bd7 | ||
|
365662a2af | ||
|
3e0ff7f43f | ||
|
8c3753326f | ||
|
dbcf6de2d5 | ||
|
a5308995b7 | ||
|
270ac88b51 | ||
|
30160e2843 | ||
|
503d22e4d0 | ||
|
b9a0d489d7 | ||
|
d9d4c4c302 | ||
|
81c6d75d62 | ||
|
d1419bdfbd | ||
|
8566d9b7e5 | ||
|
b319d6db6b | ||
|
35fd1d8455 | ||
|
be21771e49 | ||
|
745528a9b1 | ||
|
f1422af95b | ||
|
f92f4dc1e2 | ||
|
019cfdc1db | ||
|
e694a51cdd | ||
|
16453ed602 | ||
|
fde68dc598 | ||
|
22e3140ff0 | ||
|
4824c4198b | ||
|
ec7d4f4498 | ||
|
d635de44a8 | ||
|
bce66bf57f | ||
|
0fc5153f9b | ||
|
0398c2fae1 | ||
|
5fc495eb6a | ||
|
847e81e95c | ||
|
7918622e2e | ||
|
427fa1816d | ||
|
0c7e388463 | ||
|
be3af53011 | ||
|
414399b2a0 | ||
|
c592051a80 | ||
|
33a8f311e5 | ||
|
018c3cdcd4 | ||
|
384dffbf9a | ||
|
0306ecb03d | ||
|
e5ce732557 | ||
|
f19e0498bf | ||
|
e505067759 | ||
|
32ec02225a | ||
|
3920933fc7 | ||
|
1fdd7f156c | ||
|
91925cf931 | ||
|
30e58f1c19 | ||
|
8d5c6db39f | ||
|
3453f0efaf | ||
|
eb23940996 | ||
|
6cd86d86a6 | ||
|
c3fa34f5b9 | ||
|
0af0aaaa21 | ||
|
02100c502b | ||
|
b55154cd3c | ||
|
1e6e5e66da | ||
|
9b0e88ca87 | ||
|
3acc0babb7 | ||
|
dc6eda1355 | ||
|
6224bc3bca | ||
|
9ceac8c991 | ||
|
834fcd80d7 | ||
|
20e4a79130 | ||
|
d336200d75 | ||
|
e5bb3e2f0a | ||
|
5b2b2bab46 | ||
|
c17eea3ed9 | ||
|
83f4e2f5bf | ||
|
c8e7a00c08 | ||
|
190014cf96 | ||
|
385064e324 | ||
|
f3e1f088f8 | ||
|
6eb9447bb9 | ||
|
0ee8d7da0f | ||
|
918a331609 | ||
|
9bc395d36f | ||
|
41453f5bd1 | ||
|
78f62751e5 | ||
|
d59c30757c | ||
|
30d2e4fdac | ||
|
d43a074cc1 | ||
|
27783dc6de | ||
|
2a789a9a9b | ||
|
a66dba767e | ||
|
e00579e1a2 | ||
|
99f1a55de2 | ||
|
3ec307a2a1 | ||
|
3fdef9ea6d | ||
|
332c917c94 | ||
|
35b0ac78b8 | ||
|
047627a6e5 | ||
|
e4e775a107 | ||
|
5d6811bd50 | ||
|
688f215dfd | ||
|
7cbdbbcc8d | ||
|
4e89b8fbac | ||
|
70ec8cb11f | ||
|
0f0266609f | ||
|
ecfa6e994e | ||
|
ed4a45f214 | ||
|
e953be6e4a | ||
|
bd51806a0d | ||
|
625dbfbbd7 | ||
|
1c6ea0a860 | ||
|
3706cd5ff7 | ||
|
934cd1d511 | ||
|
68e3d3b989 | ||
|
31fa916156 | ||
|
456bb3ca63 | ||
|
b07ad094c1 | ||
|
d766cfaa67 | ||
|
acc616c204 | ||
|
8707f154ee | ||
|
992427f0e0 | ||
|
2c84add935 | ||
|
e8fc91fd34 | ||
|
8c9cf4a02d | ||
|
23f0969b2d | ||
|
cf82813280 | ||
|
753fa4dfa9 | ||
|
58ff338ab3 | ||
|
064e12a57b | ||
|
54bb8ed817 | ||
|
b651eae258 | ||
|
7562d9bbf8 | ||
|
1b50dd5adf | ||
|
c10a5db280 | ||
|
500690be49 | ||
|
778ac35ee6 | ||
|
c16a664a78 | ||
|
75a265f89a | ||
|
ddb55210b4 | ||
|
e093520696 | ||
|
cfacddcb36 | ||
|
d2dea3ff35 | ||
|
865fcb95bf | ||
|
a52c089728 | ||
|
15d25649b2 | ||
|
e0210635e3 | ||
|
90b993e234 | ||
|
9b7c2c50fb | ||
|
fc7a7a1549 | ||
|
a12045c19e | ||
|
fccb3a3f0c | ||
|
62ae49b639 | ||
|
2e8f58bb2f | ||
|
9e98ce220f | ||
|
10c3a0c056 | ||
|
69ec654e4a | ||
|
a310e751bb | ||
|
a9c7142d7b | ||
|
7a40c3526f | ||
|
3253d9d3fb | ||
|
3ef8b81e51 | ||
|
1f409235e4 | ||
|
806baa6ae4 | ||
|
58ed72058f | ||
|
253a994438 | ||
|
5549eded38 | ||
|
20777363cf | ||
|
b3f859f290 | ||
|
8c9f68a9c3 | ||
|
41f178bf59 | ||
|
ad5050f92e | ||
|
16397852ae | ||
|
c125a53655 | ||
|
7b056a832f | ||
|
6c40727027 | ||
|
8f06765caf | ||
|
de2fc78ad1 | ||
|
ee08410260 | ||
|
88da8d949f | ||
|
d2a92db921 | ||
|
9c18a33d7f | ||
|
f3b8258067 | ||
|
da78c73485 | ||
|
cec25ce279 | ||
|
0389007491 | ||
|
38a64bdd9e | ||
|
6a24f787c4 | ||
|
98564aa8bf | ||
|
9ceff90f42 | ||
|
6a4388fecc | ||
|
1958444bc4 | ||
|
a2038274e1 | ||
|
532c697026 | ||
|
5cf5c1e1f7 | ||
|
60621e8b81 | ||
|
a6a1f65d3e | ||
|
a681f741dd | ||
|
bed9837967 | ||
|
855cb2226a | ||
|
d85a3d17c8 | ||
|
81a5b154c2 | ||
|
a6f42df9d6 | ||
|
17ae7acc6d | ||
|
3d5fec3c30 | ||
|
21f48fba5f | ||
|
d0da5d7c48 | ||
|
4209cc6ee4 | ||
|
f3e1092dd5 | ||
|
43055b666f | ||
|
7cd8ed6e23 | ||
|
4bc0edeeca | ||
|
7a06df6ccd | ||
|
d9164a32e5 | ||
|
a8fbf32a88 | ||
|
514689d2f4 | ||
|
76a15bb026 | ||
|
b061f188f9 | ||
|
af7d7d0966 | ||
|
2ffd0770c6 | ||
|
e3abeafc6b | ||
|
1720582f33 | ||
|
d93e9d570f | ||
|
5ea90f2fdd | ||
|
c628f97d8c | ||
|
8a1e2f4111 | ||
|
41f244d549 | ||
|
79641efa87 | ||
|
ca3fcc3c67 | ||
|
de8e5ec920 | ||
|
f833e44bd5 | ||
|
8b0b1db5a4 | ||
|
5b95f3af02 | ||
|
3cc66609e8 | ||
|
ce468d22dd | ||
|
3e19516f62 | ||
|
84dba739d9 | ||
|
99315c7378 | ||
|
1ca5b864a9 | ||
|
96eb60dca3 | ||
|
c67fcb0034 | ||
|
273dfe2365 | ||
|
5952ea4b54 | ||
|
1708ba077c | ||
|
b4301f125d | ||
|
9e52f20f71 | ||
|
31bf388cab | ||
|
276c479a5f | ||
|
9393689037 | ||
|
76485010ad | ||
|
b8ac128d5c | ||
|
06c75310a1 | ||
|
b9d819220d | ||
|
7a569f342f | ||
|
f1efae79c8 | ||
|
1cd4bbc078 | ||
|
0b5e5832c4 | ||
|
7db96c672f | ||
|
f8725d6313 | ||
|
2165c87b9d | ||
|
f61e320230 | ||
|
6c1b21da22 | ||
|
f6216b9745 | ||
|
91e82d1e3c | ||
|
50e2bda020 | ||
|
ab1de1aafa | ||
|
d4ac87f91d | ||
|
52f4008725 | ||
|
d8dd71ff50 | ||
|
02966bf39a | ||
|
a83d8bf1d5 | ||
|
1e4432b1f3 | ||
|
d50c979d9f | ||
|
4e60ec5192 | ||
|
31c4e3b1be | ||
|
b97fbd3171 | ||
|
43fb5d9332 | ||
|
ea3f1a8eff | ||
|
7bb060781e | ||
|
a1ab0b7c31 | ||
|
fae2afd010 | ||
|
76c0a5a4fa | ||
|
cdb65657b6 | ||
|
9007dfe779 | ||
|
99d09a9877 | ||
|
afcf51399e | ||
|
47679b7f6c | ||
|
8f2985862d | ||
|
23a524b464 | ||
|
ce8992867d | ||
|
c89de7adfb | ||
|
cac68ccc14 | ||
|
d507383487 | ||
|
ce5cf87094 | ||
|
ec2c9d2a44 | ||
|
36d0abe635 | ||
|
615381ca5a | ||
|
6b83499216 | ||
|
faa2e9502b | ||
|
cd07240ce7 | ||
|
91518464c2 | ||
|
25bf315e23 | ||
|
a8743b1bd3 | ||
|
f90bd6ee45 | ||
|
a351756883 | ||
|
ea9b0d4588 | ||
|
e9c85c946e | ||
|
876bfb525b | ||
|
6be67d0d92 | ||
|
16581766c6 | ||
|
7fd7104f46 | ||
|
d2cae7c8b5 | ||
|
83955acd22 | ||
|
d85b746021 | ||
|
e2dc91c1ea | ||
|
098e2623d6 | ||
|
2df045396d | ||
|
6c182a3567 | ||
|
340d6dfc8d | ||
|
eb27b0d3e2 | ||
|
7377893050 | ||
|
21d7a4eed1 | ||
|
fb2c2b1d09 | ||
|
6f2dd6c8d0 | ||
|
4cc0e7845f | ||
|
ca8eabc425 | ||
|
94c83d6a93 | ||
|
dea2f34c52 | ||
|
b294f5e333 | ||
|
f9b726a75d | ||
|
579645d6b7 | ||
|
e729571a21 | ||
|
f179139967 | ||
|
ee5119e4dd | ||
|
11bf5ac6fc | ||
|
c3eb385066 | ||
|
886c9156a5 | ||
|
23062c59cd | ||
|
da254058c5 | ||
|
a6fa4fc613 | ||
|
593b61ea4b | ||
|
04d691991a | ||
|
faa6e82bef | ||
|
65ccb80aa4 | ||
|
cc13d1d0f1 | ||
|
ead84d70d1 | ||
|
275146fb55 | ||
|
d81729d9f7 | ||
|
e74a249388 | ||
|
d8c5e557d8 | ||
|
984e8b5227 | ||
|
469bf2a63e | ||
|
e05fcd6655 | ||
|
3a7751910e | ||
|
3c139f3e53 | ||
|
2c14a12464 | ||
|
faa6405d5f | ||
|
e2e4555009 | ||
|
448e984995 | ||
|
ec1954bae1 | ||
|
66276a6f65 | ||
|
c00ab2f87c | ||
|
99f3ab9921 | ||
|
75b15c6639 | ||
|
e5e60a5032 | ||
|
b9d2bbb78b | ||
|
3d79408574 | ||
|
67b184acc9 | ||
|
11040ae60a | ||
|
a338202ded | ||
|
e0398490ae | ||
|
847093edac | ||
|
758a42db36 | ||
|
c2b253df55 | ||
|
cc3adc6720 | ||
|
d77acd9eb9 | ||
|
5ad2bc1940 | ||
|
ea4c51026b | ||
|
d0b4a1f12f | ||
|
50b912e229 | ||
|
5ffddd0169 | ||
|
b5512dfa91 | ||
|
2493d5f7a8 | ||
|
bef1828404 | ||
|
0ae232b8ba | ||
|
cdf0795881 | ||
|
31e85226f4 | ||
|
8df3c68df9 | ||
|
ca0deb2bf6 | ||
|
6eea7c2aa1 | ||
|
9aabf93523 | ||
|
322e2cdc41 | ||
|
df395c77db | ||
|
274002c473 | ||
|
3d4fc38fcb | ||
|
d50f16b8a9 | ||
|
59de22883b | ||
|
db73afaf88 | ||
|
3afbd7aa51 | ||
|
0c32afa07f | ||
|
bd543a526b | ||
|
b262044a52 | ||
|
200a97184c | ||
|
0164bca2d7 | ||
|
5f4b66e5b0 | ||
|
9c48343581 | ||
|
219f82f562 | ||
|
51bfd49e25 | ||
|
7102f4a727 | ||
|
28991a926f | ||
|
74f89af841 | ||
|
b4284515e7 | ||
|
032c5d0d07 | ||
|
72100fe1d8 | ||
|
ed178e744d | ||
|
94f7079c2e | ||
|
f1638aa9d9 | ||
|
a7b9215ace | ||
|
956ad7bfa8 | ||
|
4baf442cb8 | ||
|
24eb7c6933 | ||
|
38a2af8cd5 | ||
|
7447ab20a9 | ||
|
197c742ce7 | ||
|
f6ed3388a4 | ||
|
c61690f3e6 | ||
|
9a94873d0e | ||
|
5c8bda37d1 | ||
|
fbb73ee88e | ||
|
fa08041696 | ||
|
f01a34a461 | ||
|
c0242fe78b | ||
|
ef790a8cb1 | ||
|
cea0cee9a8 | ||
|
d9f784ed2b | ||
|
f29ae656b9 | ||
|
a9852e5305 | ||
|
32470fa641 | ||
|
0d1c08bf96 | ||
|
026c904ce4 | ||
|
59ae070c83 | ||
|
2ab695f308 | ||
|
f0ff77cd31 | ||
|
e241d9fa67 | ||
|
5e9b0cd03c | ||
|
a5c437913f | ||
|
3fa7da5115 | ||
|
6d8f41ab05 | ||
|
c7c419a3cd | ||
|
3094ec3c26 | ||
|
359a35931e | ||
|
9e0001c4f6 | ||
|
53112709fe | ||
|
d1bc500ab9 | ||
|
74b5401e84 | ||
|
d2559f36ce | ||
|
0cc9cd728e | ||
|
e5cf76f3e0 | ||
|
44e8b3e6e7 | ||
|
1bca152d9c | ||
|
4edc3ee121 | ||
|
e9f29767c8 | ||
|
e2916b130b | ||
|
538e117abd | ||
|
71e80bd02f | ||
|
800103225b | ||
|
cfb7a3cc4c | ||
|
3185cf23df | ||
|
3ac1b36549 | ||
|
55e99062ca | ||
|
edb56b74da | ||
|
233ace23de | ||
|
e1f3b9a7a3 | ||
|
6be88ac86e | ||
|
d03cca2e76 | ||
|
e86143426b | ||
|
a309f8de9e | ||
|
df15f81b9f | ||
|
6ab50e4dff | ||
|
476d9e4c95 | ||
|
9eaf31707a | ||
|
e0007269b9 | ||
|
0006911e0a | ||
|
e141e98ecc | ||
|
40531284dd | ||
|
9e2663b11b | ||
|
2602bb01e1 | ||
|
ac15dfe789 | ||
|
cd066cf357 | ||
|
bd521baade | ||
|
136188339d | ||
|
48de33fe24 | ||
|
df4798e2a1 | ||
|
edb842f0d1 | ||
|
b563fe567d | ||
|
b3dd0d89df | ||
|
360f6cd4f1 | ||
|
f1e4528581 | ||
|
1375372380 | ||
|
a7d0ad27b1 | ||
|
833a785996 | ||
|
94d18538d8 | ||
|
8baf42765e | ||
|
b93dca1818 | ||
|
98506a038b | ||
|
3eaeb927ec | ||
|
7ded8954c6 | ||
|
d2eb8b0c2b | ||
|
2ed2cc0fab | ||
|
87bb9090f5 | ||
|
8844cc0076 | ||
|
2c4bad6455 | ||
|
5c0134c16a | ||
|
369a45f5c0 | ||
|
d58b7c1f53 | ||
|
5011a2be22 | ||
|
33010f82d8 | ||
|
8dbd8c4e65 | ||
|
c2a3c1271c | ||
|
1e9a7ac87d | ||
|
e25d24964c | ||
|
d90d84ab06 | ||
|
2ddb1fa23e | ||
|
82f6e38adb | ||
|
00ac26cf27 | ||
|
fa1aa1c8b2 | ||
|
9156325ffc | ||
|
70131914b2 | ||
|
bd87e6cc1a | ||
|
6c8e96daae | ||
|
cfb8f53849 | ||
|
7bb2f841ad | ||
|
558263521c | ||
|
1db8c06291 | ||
|
edee08a7b5 | ||
|
0594bdf1c7 | ||
|
72c299a331 | ||
|
0b42a7ad63 | ||
|
51e59f6ab7 | ||
|
38394de661 | ||
|
22f9755f4f | ||
|
4cb2e113cb | ||
|
e0f024ef5c | ||
|
f95cb99cdc | ||
|
1f0170bb4b | ||
|
5eafdf3c80 | ||
|
d7c26c27e1 | ||
|
2d18dd62eb | ||
|
51fd177d79 | ||
|
c4643538f1 | ||
|
0edadaf901 | ||
|
c05533fb71 | ||
|
db270f559f | ||
|
79f7f68b0c | ||
|
d30d61edab | ||
|
f7e2ee383e | ||
|
544fdbfbe9 | ||
|
c3d1ecb7e1 | ||
|
873a9d0cee | ||
|
95cb5f607b | ||
|
54d0b721c4 | ||
|
4a4678b331 | ||
|
12f9ecaaca | ||
|
1d3ee77fb8 | ||
|
83bce3ce52 | ||
|
bf288dbfc2 | ||
|
64055e280d | ||
|
90e0f9dddc | ||
|
b0b0e2ac18 | ||
|
9db606af5a | ||
|
5bfc17bd64 | ||
|
a3c44428d3 | ||
|
450ff7318f | ||
|
2dffd382ae | ||
|
e425fe5d5a | ||
|
a68caa2de1 | ||
|
3470b8bf57 | ||
|
99e11d1f52 | ||
|
743db9190e | ||
|
53b3d09288 | ||
|
4565d0ddcb | ||
|
de51de1819 |
33
.github/workflows/pr-testing.yml
vendored
Normal file
33
.github/workflows/pr-testing.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: PR testing
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
mavenTesting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
- name: Set up the Java JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Run all tests
|
||||
run: |
|
||||
mvn -B clean test -DskipTests=false --file pom.xml
|
||||
if [ -f "target/site/jacoco/index.html" ]; then echo "Total coverage: $(cat target/site/jacoco/index.html | grep -o 'Total[^%]*%' | grep -o '[0-9]*%')"; fi
|
||||
|
||||
- name: Log coverage percentage
|
||||
run: |
|
||||
if [ ! -f "target/site/jacoco/index.html" ]; then echo "No coverage information available"; fi
|
||||
if [ -f "target/site/jacoco/index.html" ]; then echo "Total coverage: $(cat target/site/jacoco/index.html | grep -o 'Total[^%]*%' | grep -o '[0-9]*%')"; fi
|
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
/db*
|
||||
/lists/
|
||||
/bin/
|
||||
/target/
|
||||
/qortal-backup/
|
||||
/log.txt.*
|
||||
/arbitrary*
|
||||
/Qortal-BTC*
|
||||
@@ -14,5 +16,19 @@
|
||||
/settings.json
|
||||
/testnet*
|
||||
/settings*.json
|
||||
/testchain.json
|
||||
/run-testnet.sh
|
||||
/testchain*.json
|
||||
/run-testnet*.sh
|
||||
/.idea
|
||||
/qortal.iml
|
||||
.DS_Store
|
||||
/src/main/resources/resources
|
||||
/*.jar
|
||||
/run.pid
|
||||
/run.log
|
||||
/WindowsInstaller/Install Files/qortal.jar
|
||||
/*.7z
|
||||
/tmp
|
||||
/wallets
|
||||
/data*
|
||||
/src/test/resources/arbitrary/*/.qortal/cache
|
||||
apikey.txt
|
||||
|
@@ -1,5 +1,20 @@
|
||||
# Auto Updates
|
||||
|
||||
## TL;DR: how-to
|
||||
|
||||
* Prepare new release version (see way below for details)
|
||||
* Assuming you are in git 'master' branch, at HEAD
|
||||
* Shutdown local node if running
|
||||
* Build auto-update download: `tools/build-auto-update.sh` - uploads auto-update file into new git branch
|
||||
* Restart local node
|
||||
* Publish auto-update transaction using *private key* for **non-admin** member of "dev" group:
|
||||
`tools/publish-auto-update.sh non-admin-dev-member-private-key-in-base58`
|
||||
* Wait for auto-update `ARBITRARY` transaction to be confirmed into a block
|
||||
* Have "dev" group admins 'approve' auto-update using `tools/approve-auto-update.sh`
|
||||
This tool will prompt for *private key* of **admin** of "dev" group
|
||||
* A minimum number of admins are required for approval, and a minimum number of blocks must pass also.
|
||||
* Nodes will start to download, and apply, the update over the next 20 minutes or so (see CHECK_INTERVAL in AutoUpdate.java)
|
||||
|
||||
## Theory
|
||||
* Using a specific git commit (e.g. abcdef123) we produce a determinstic JAR with consistent hash.
|
||||
* To avoid issues with over-eager anti-virus / firewalls we obfuscate JAR using very simplistic XOR-based method.
|
||||
@@ -25,8 +40,8 @@ The same method is used to obfuscate and de-obfuscate:
|
||||
|
||||
## Typical download locations
|
||||
The git SHA1 commit hash is used to replace `%s` in various download locations, e.g.:
|
||||
* https://github.com/QORT/qortal/raw/%s/qortal.update
|
||||
* https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update
|
||||
* https://github.com/Qortal/qortal/raw/%s/qortal.update
|
||||
* https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update
|
||||
|
||||
These locations are part of the org.qortal.settings.Settings class and can be overriden in settings.json like:
|
||||
```
|
||||
@@ -45,4 +60,12 @@ $ java -cp qortal.jar org.qortal.XorUpdate
|
||||
usage: XorUpdate <input-file> <output-file>
|
||||
$ java -cp qortal.jar org.qortal.XorUpdate qortal.jar qortal.update
|
||||
$
|
||||
```
|
||||
```
|
||||
|
||||
## Preparing new release version
|
||||
|
||||
* Shutdown local node
|
||||
* Modify `pom.xml` and increase version inside `<version>` tag
|
||||
* Commit new `pom.xml` and push to github, e.g. `git commit -m 'Bumped to v1.4.2' -- pom.xml; git push`
|
||||
* Tag this new commit with same version: `git tag v1.4.2`
|
||||
* Push tag up to github: `git push origin v1.4.2`
|
||||
|
12
DATABASE.md
12
DATABASE.md
@@ -4,10 +4,10 @@ You can examine your node's database using [HSQLDB's "sqltool"](http://www.hsqld
|
||||
It's a good idea to install "rlwrap" (ReadLine wrapper) too as sqltool doesn't support command history/editing.
|
||||
|
||||
Typical command line for sqltool would be:
|
||||
`rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC} qora`
|
||||
`rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC} qortal`
|
||||
|
||||
`${HSQLDB_JAR}` should be set with pathname where Maven downloaded hsqldb,
|
||||
typically `${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.0/hsqldb-2.5.0.jar`
|
||||
typically `${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.1/hsqldb-2.5.1.jar`
|
||||
|
||||
`${SQLTOOL_JAR}` should be set with pathname where Maven downloaded sqltool,
|
||||
typically `${HOME}/.m2/repository/org/hsqldb/sqltool/2.5.0/sqltool-2.5.0.jar`
|
||||
@@ -25,10 +25,16 @@ Above `url` component `file:db/blockchain` assumes you will call `sqltool` from
|
||||
|
||||
Another idea is to assign a shell alias in your `.bashrc` like:
|
||||
```
|
||||
export HSQLDB_JAR=${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.0/hsqldb-2.5.0.jar
|
||||
export HSQLDB_JAR=${HOME}/.m2/repository/org/hsqldb/hsqldb/2.5.1/hsqldb-2.5.1.jar
|
||||
export SQLTOOL_JAR=${HOME}/.m2/repository/org/hsqldb/sqltool/2.5.0/sqltool-2.5.0.jar
|
||||
alias sqltool='rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC}'
|
||||
```
|
||||
So you can simply type: `sqltool qortal`
|
||||
|
||||
Don't forget to use `SHUTDOWN;` before exiting sqltool so that database files are closed cleanly.
|
||||
|
||||
## Quick and dirty version
|
||||
|
||||
With `sqltool-2.5.0.jar` and `qortal.jar` in current directory, and database in `db/`
|
||||
|
||||
`java -cp qortal.jar:sqltool-2.5.0.jar org.hsqldb.cmdline.SqlTool --inlineRc=url=jdbc:hsqldb:file:db/blockchain`
|
||||
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM maven:3-openjdk-11 as builder
|
||||
|
||||
WORKDIR /work
|
||||
COPY ./ /work/
|
||||
RUN mvn clean package
|
||||
|
||||
###
|
||||
FROM openjdk:11
|
||||
|
||||
RUN useradd -r -u 1000 -g users qortal && \
|
||||
mkdir /usr/local/qortal /qortal && \
|
||||
chown 1000:100 /qortal
|
||||
|
||||
COPY --from=builder /work/log4j2.properties /usr/local/qortal/
|
||||
COPY --from=builder /work/target/qortal*.jar /usr/local/qortal/qortal.jar
|
||||
|
||||
USER 1000:100
|
||||
|
||||
EXPOSE 12391 12392
|
||||
HEALTHCHECK --start-period=5m CMD curl -sf http://127.0.0.1:12391/admin/info || exit 1
|
||||
|
||||
WORKDIR /qortal
|
||||
VOLUME /qortal
|
||||
|
||||
ENTRYPOINT ["java"]
|
||||
CMD ["-Djava.net.preferIPv4Stack=false", "-jar", "/usr/local/qortal/qortal.jar"]
|
@@ -9,4 +9,4 @@
|
||||
- Create basic *settings.json* file: `echo '{}' > settings.json`
|
||||
- Run JAR in same working directory as *settings.json*: `java -jar target/qortal-1.0.jar`
|
||||
- Wrap in shell script, add JVM flags, redirection, backgrounding, etc. as necessary.
|
||||
- Or use supplied example shell script: *run.sh*
|
||||
- Or use supplied example shell script: *start.sh*
|
||||
|
95
TestNets.md
Normal file
95
TestNets.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# How to build a testnet
|
||||
|
||||
## Create testnet blockchain config
|
||||
|
||||
- You can begin by copying the mainnet blockchain config `src/main/resources/blockchain.json`
|
||||
- Insert `"isTestChain": true,` after the opening `{`
|
||||
- Modify testnet genesis block
|
||||
|
||||
### Testnet genesis block
|
||||
|
||||
- Set `timestamp` to a nearby future value, e.g. 15 mins from 'now'
|
||||
This is to give yourself enough time to set up other testnet nodes
|
||||
- Retain the initial `ISSUE_ASSET` transactions!
|
||||
- Add `ACCOUNT_FLAGS` transactions with `"andMask": -1, "orMask": 1, "xorMask": 0` to create founders
|
||||
- Add at least one `REWARD_SHARE` transaction otherwise no-one can mint initial blocks!
|
||||
You will need to calculate `rewardSharePublicKey` (and private key),
|
||||
or make a new account on mainnet and use self-share key values
|
||||
- Add `ACCOUNT_LEVEL` transactions to set initial level of accounts as needed
|
||||
- Add `GENESIS` transactions to add QORT/LEGACY_QORA funds to accounts as needed
|
||||
|
||||
## Testnet `settings.json`
|
||||
|
||||
- Create a new `settings-test.json`
|
||||
- Make sure to add `"isTestNet": true,`
|
||||
- Make sure to reference testnet blockchain config file: `"blockchainConfig": "testchain.json",`
|
||||
- It is a good idea to use a separate database: `"repositoryPath": "db-testnet",`
|
||||
- You might also need to add `"bitcoinNet": "TEST3",` and `"litecoinNet": "TEST3",`
|
||||
|
||||
## Other nodes
|
||||
|
||||
- Copy `testchain.json` and `settings-test.json` to other nodes
|
||||
- Alternatively, you can run multiple nodes on the same machine by:
|
||||
* Copying `settings-test.json` to `settings-test-1.json`
|
||||
* Configure different `repositoryPath`
|
||||
* Configure use of different ports:
|
||||
+ `"apiPort": 22391,`
|
||||
+ `"listenPort": 22392,`
|
||||
|
||||
## Starting-up
|
||||
|
||||
- Start up at least as many nodes as `minBlockchainPeers` (or adjust this value instead)
|
||||
- Probably best to perform API call `DELETE /peers/known`
|
||||
- Add other nodes via API call `POST /peers <peer-hostname-or-IP>`
|
||||
- Add minting private key to nodes via API call `POST /admin/mintingaccounts <minting-private-key>`
|
||||
The keys must have corresponding `REWARD_SHARE` transactions in testnet genesis block
|
||||
- You must have at least 2 separate minting keys and two separate nodes. Assign one minting key to each node.
|
||||
- Alternatively, comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java to allow for a single node and key.
|
||||
- Wait for genesis block timestamp to pass
|
||||
- A node should mint block 2 approximately 60 seconds after genesis block timestamp
|
||||
- Other testnet nodes will sync *as long as there is at least `minBlockchainPeers` peers with an "up-to-date" chain`
|
||||
- You can also use API call `POST /admin/forcesync <connected-peer-IP-and-port>` on stuck nodes
|
||||
|
||||
## Single-node testnet
|
||||
|
||||
A single-node testnet is possible with code modifications, for basic testing, or to more easily start a new testnet.
|
||||
To do so, follow these steps:
|
||||
- Comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java
|
||||
- Comment out the `minBlockchainPeers` validation in Settings.validate()
|
||||
- Set `minBlockchainPeers` to 0 in settings.json
|
||||
- Set `Synchronizer.RECOVERY_MODE_TIMEOUT` to `0`
|
||||
- All other steps should remain the same. Only a single reward share key is needed.
|
||||
- Remember to put these values back after introducing other nodes
|
||||
|
||||
## Fixed network
|
||||
|
||||
To restrict a testnet to a set of private nodes, you can use the "fixed network" feature.
|
||||
This ensures that the testnet nodes only communicate with each other and not other known peers.
|
||||
To do this, add the following setting to each testnet node, substituting the IP addresses:
|
||||
```
|
||||
"fixedNetwork": [
|
||||
"192.168.0.101:62392",
|
||||
"192.168.0.102:62392",
|
||||
"192.168.0.103:62392"
|
||||
]
|
||||
```
|
||||
|
||||
## Dealing with stuck chain
|
||||
|
||||
Maybe your nodes have been offline and no-one has minted a recent testnet block.
|
||||
Your options are:
|
||||
|
||||
- Start a new testnet from scratch
|
||||
- Fire up your testnet node(s)
|
||||
- Force one of your nodes to mint by:
|
||||
+ Set a debugger breakpoint on Settings.getMinBlockchainPeers()
|
||||
+ When breakpoint is hit, change `this.minBlockchainPeers` to zero, then continue
|
||||
- Once one of your nodes has minted blocks up to 'now', you can use "forcesync" on the other nodes
|
||||
|
||||
## Tools
|
||||
|
||||
- `qort` tool, but use `-t` option for default testnet API port (62391)
|
||||
- `qort` tool, but first set shell variable: `export BASE_URL=some-node-hostname-or-ip:port`
|
||||
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
|
||||
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
|
||||
|
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
3
WindowsInstaller/Install Files/AppData/settings.json
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"apiDocumentationEnabled": true
|
||||
}
|
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
70
WindowsInstaller/Install Files/log4j2.properties
Executable file
@@ -0,0 +1,70 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment next line to set dirname:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
|
||||
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
|
||||
# Suppress extraneous bitcoinj library output
|
||||
logger.bitcoinj.name = org.bitcoinj
|
||||
logger.bitcoinj.level = error
|
||||
|
||||
# Override HSQLDB logging level to "warn" as too much is logged at "info"
|
||||
logger.hsqldb.name = hsqldb.db
|
||||
logger.hsqldb.level = warn
|
||||
|
||||
# Support optional, per-session HSQLDB debugging
|
||||
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
|
||||
logger.hsqldbRepository.level = debug
|
||||
|
||||
# Suppress extraneous Jersey warning
|
||||
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
|
||||
logger.jerseyInject.level = off
|
||||
|
||||
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
|
||||
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
|
||||
logger.jerseyEOF.level = off
|
||||
|
||||
# Suppress extraneous Jetty entries
|
||||
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
|
||||
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
|
||||
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
|
||||
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
|
||||
logger.jetty.name = org.eclipse.jetty
|
||||
logger.jetty.level = warn
|
||||
# Even more extraneous Jetty output
|
||||
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
|
||||
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
|
||||
logger.jettyRS.level = error
|
||||
|
||||
# Suppress extraneous slf4j entries
|
||||
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
|
||||
logger.slf4j.name = org.slf4j
|
||||
logger.slf4j.level = warn
|
||||
|
||||
# Suppress extraneous Reflections entry
|
||||
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
|
||||
logger.orgReflections.name = org.reflections.Reflections
|
||||
logger.orgReflections.level = off
|
||||
logger.sunReflections.name = sun.reflect.Reflection
|
||||
logger.sunReflections.level = off
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = stdout
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.console.filter.threshold.type = ThresholdFilter
|
||||
appender.console.filter.threshold.level = error
|
||||
|
||||
appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ./${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
# Set the immediate flush to true (default)
|
||||
# appender.rolling.immediateFlush = true
|
||||
# Set the append to true (default), should not overwrite
|
||||
# appender.rolling.append=true
|
1569
WindowsInstaller/Qortal.aip
Executable file
1569
WindowsInstaller/Qortal.aip
Executable file
File diff suppressed because it is too large
Load Diff
26
WindowsInstaller/README.md
Normal file
26
WindowsInstaller/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Windows installer
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
|
||||
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
|
||||
|
||||
## General build instructions
|
||||
|
||||
If this is your first time opening the `qortal.aip` file then you might need to adjust
|
||||
configured paths, or create a dummy `D:` drive with the expected layout.
|
||||
|
||||
Typical build procedure:
|
||||
|
||||
* Place the `qortal.jar` file in `Install-Files\`
|
||||
* Open AdvancedInstaller with qortal.aip file
|
||||
* If releasing a new version, change version number in:
|
||||
+ "Product Information" side menu
|
||||
+ "Product Details" side menu entry
|
||||
+ "Product Details" tab in "Product Details" pane
|
||||
+ "Product Version" entry box
|
||||
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
|
||||
* You should be prompted whether to generate a new product key, click "Generate New"
|
||||
* Click "Build" button
|
||||
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number
|
||||
|
117
WindowsInstaller/dictionary.ail
Normal file
117
WindowsInstaller/dictionary.ail
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<DICTIONARY type="multilanguage">
|
||||
<!-- Control table -->
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
|
||||
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="ru" value="Выберите один из вариантов ниже, затем нажмите Далее"/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="ru" value="Выберите место хранения данных."/>
|
||||
<STRING lang="zh" value="选择数据保存的文件夹?"/>
|
||||
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
|
||||
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
|
||||
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
|
||||
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="ru" value="Выберите место хранения данных."/>
|
||||
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
|
||||
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
|
||||
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Text">
|
||||
<STRING lang="en" value="To store data in this folder, click "[Text_Next]". To store data in a different folder, enter it below or click "Browse"."/>
|
||||
<STRING lang="ru" value="Чтобы сохранить данные в этой папке, нажмите "[Text_Next]". Чтобы сохранить данные в другой папке, введите ее ниже или нажмите "Обзор"."/>
|
||||
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DataFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="ru" value="Выберите папку для хранения данных"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Description">
|
||||
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
|
||||
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.DbFolderDlg#Title">
|
||||
<STRING lang="en" value="Select Data Storage Folder"/>
|
||||
<STRING lang="ru" value="Выберите папку для хранения данных"/>
|
||||
<STRING lang="zh" value="请选择文件存储地方"/>
|
||||
<STRING lang="zh_TW" value="请选择文件存储地方"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Description">
|
||||
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
|
||||
<STRING lang="ru" value="Настроить синхронизацию времени системы Windows?"/>
|
||||
<STRING lang="zh" value="重新配置Windows以获得更准确的时间?"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_1">
|
||||
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
|
||||
<STRING lang="ru" value="Для подключения к сети Qortal и совершения транзакций требуется точная настройка времени Windows"/>
|
||||
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_2">
|
||||
<STRING lang="en" value="Select one of the options below, then click "Next"."/>
|
||||
<STRING lang="ru" value="Выберите один из вариантов ниже, затем нажмите"/>
|
||||
<STRING lang="zh" value="请选择,然后“下一步”"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Text_3">
|
||||
<STRING lang="en" value="Your computer's clock needs to be accurate to within 0.5 seconds."/>
|
||||
<STRING lang="ru" value="Точность времени вашего компьютера должна составлять 0.5 секунд."/>
|
||||
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.NTPDialog#Title">
|
||||
<STRING lang="en" value="Windows clock accuracy"/>
|
||||
<STRING lang="ru" value="Настройка времени системы Windows"/>
|
||||
<STRING lang="zh" value="Windows 时钟精度"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
|
||||
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
|
||||
<STRING lang="ru" value="Удалить загруженный блокчейн и другие данные"/>
|
||||
<STRING lang="zh" value="删除您下载的区块链"/>
|
||||
</ENTRY>
|
||||
<!-- RadioButton table -->
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
|
||||
<STRING lang="en" value="Choose custom data storage folder..."/>
|
||||
<STRING lang="ru" value="Выбрать папку для хранения данных..."/>
|
||||
<STRING lang="zh" value="选择特定的文件夹存储"/>
|
||||
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
|
||||
<STRING lang="en" value="Use default location "/>
|
||||
<STRING lang="ru" value="Использовать папку по умолчанию"/>
|
||||
<STRING lang="zh" value="使用默认存储地点"/>
|
||||
<STRING lang="zh_TW" value="使用默认存储地点"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
|
||||
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
|
||||
<STRING lang="ru" value="Да, настроить синхронизацию времени Windows (Рекомендуется)"/>
|
||||
<STRING lang="zh" value="是,将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
|
||||
</ENTRY>
|
||||
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
|
||||
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
|
||||
<STRING lang="ru" value="Нет, я сам буду управлять настройками часов"/>
|
||||
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
|
||||
</ENTRY>
|
||||
</DICTIONARY>
|
BIN
WindowsInstaller/qortal.ico
Normal file
BIN
WindowsInstaller/qortal.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.jar
Normal file
BIN
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.jar
Normal file
Binary file not shown.
9
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.pom
Normal file
9
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<version>1.1</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
12
lib/com/dosse/WaifUPnP/maven-metadata-local.xml
Normal file
12
lib/com/dosse/WaifUPnP/maven-metadata-local.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<versioning>
|
||||
<release>1.1</release>
|
||||
<versions>
|
||||
<version>1.1</version>
|
||||
</versions>
|
||||
<lastUpdated>20220218200127</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
BIN
lib/org/ciyam/AT/1.3.7/AT-1.3.7.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.7/AT-1.3.7.jar
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.0</version>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.7</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
BIN
lib/org/ciyam/AT/1.3.8/AT-1.3.8.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
9
lib/org/ciyam/AT/1.3.8/AT-1.3.8.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.8</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
16
lib/org/ciyam/AT/maven-metadata-local.xml
Normal file
16
lib/org/ciyam/AT/maven-metadata-local.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.8</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
</versions>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<versioning>
|
||||
<release>1.0</release>
|
||||
<versions>
|
||||
<version>1.0</version>
|
||||
</versions>
|
||||
<lastUpdated>20181105100741</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed-sources.jar
Normal file
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed-sources.jar
Normal file
Binary file not shown.
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.jar
Normal file
BIN
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.jar
Normal file
Binary file not shown.
9
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.pom
Normal file
9
lib/org/hsqldb/hsqldb/2.5.0-fixed/hsqldb-2.5.0-fixed.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.5.0-fixed</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
12
lib/org/hsqldb/hsqldb/maven-metadata-local.xml
Normal file
12
lib/org/hsqldb/hsqldb/maven-metadata-local.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<versioning>
|
||||
<release>2.5.0-fixed</release>
|
||||
<versions>
|
||||
<version>2.5.0-fixed</version>
|
||||
</versions>
|
||||
<lastUpdated>20200318133132</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
@@ -1,11 +1,15 @@
|
||||
rootLogger.level = info
|
||||
# On Windows, uncomment this:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Roaming\\qortal\\
|
||||
# On Windows, uncomment next line to set dirname:
|
||||
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
|
||||
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
|
||||
|
||||
rootLogger.appenderRef.console.ref = stdout
|
||||
rootLogger.appenderRef.rolling.ref = FILE
|
||||
|
||||
# Suppress extraneous bitcoinj library output
|
||||
logger.bitcoinj.name = org.bitcoinj
|
||||
logger.bitcoinj.level = error
|
||||
|
||||
# Override HSQLDB logging level to "warn" as too much is logged at "info"
|
||||
logger.hsqldb.name = hsqldb.db
|
||||
logger.hsqldb.level = warn
|
||||
@@ -57,7 +61,7 @@ appender.rolling.type = RollingFile
|
||||
appender.rolling.name = FILE
|
||||
appender.rolling.layout.type = PatternLayout
|
||||
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
appender.rolling.filePattern = ${dirname:-}${filename}.%i
|
||||
appender.rolling.filePattern = ./${filename}.%i
|
||||
appender.rolling.policy.type = SizeBasedTriggeringPolicy
|
||||
appender.rolling.policy.size = 4MB
|
||||
# Set the immediate flush to true (default)
|
||||
|
154
pom.xml
154
pom.xml
@@ -3,26 +3,39 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>1.0</version>
|
||||
<version>3.5.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<bitcoin.version>0.15.4</bitcoin.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<skipTests>true</skipTests>
|
||||
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<commons-io.version>2.6</commons-io.version>
|
||||
<commons-compress.version>1.21</commons-compress.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
<xz.version>1.9</xz.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.0</hsqldb.version>
|
||||
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<homoglyph.version>1.2.1</homoglyph.version>
|
||||
<icu4j.version>70.1</icu4j.version>
|
||||
<upnp.version>1.1</upnp.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.22.v20191022</jetty.version>
|
||||
<log4j.version>2.12.1</log4j.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.17.1</log4j.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<slf4j.version>1.7.12</slf4j.version>
|
||||
<swagger-api.version>2.0.9</swagger-api.version>
|
||||
<swagger-ui.version>3.23.8</swagger-ui.version>
|
||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||
<jsoup.version>1.13.1</jsoup.version>
|
||||
<java-diff-utils.version>4.10</java-diff-utils.version>
|
||||
<grpc.version>1.45.1</grpc.version>
|
||||
<protobuf.version>3.19.4</protobuf.version>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
@@ -198,6 +211,10 @@
|
||||
<pattern>org.qortal.api.model**</pattern>
|
||||
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||
</package>
|
||||
<package>
|
||||
<pattern>org.qortal.api.model.**</pattern>
|
||||
<template>${project.build.sourceDirectory}/org/qortal/data/package-info.java</template>
|
||||
</package>
|
||||
</packages>
|
||||
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
|
||||
</configuration>
|
||||
@@ -257,6 +274,8 @@
|
||||
<!-- Don't include original swagger-UI as we're including our own
|
||||
modified version -->
|
||||
<exclude>org.webjars:swagger-ui</exclude>
|
||||
<!-- Don't include JUnit as it's for testing only! -->
|
||||
<exclude>junit:junit</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
@@ -310,6 +329,14 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
@@ -372,6 +399,11 @@
|
||||
<name>project</name>
|
||||
<url>file:${project.basedir}/lib</url>
|
||||
</repository>
|
||||
<!-- jitpack for build-on-demand of altcoinj -->
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/org.codehaus.mojo/build-helper-maven-plugin -->
|
||||
@@ -379,12 +411,14 @@
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
|
||||
<dependency>
|
||||
<groupId>com.github.bohnman</groupId>
|
||||
<artifactId>package-info-maven-plugin</artifactId>
|
||||
<version>${package-info-maven-plugin.version}</version>
|
||||
<scope>provided</scope><!-- needed for build, not for runtime -->
|
||||
</dependency>
|
||||
<!-- HSQLDB for repository -->
|
||||
<dependency>
|
||||
@@ -392,23 +426,29 @@
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>${hsqldb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>sqltool</artifactId>
|
||||
<version>${hsqldb-sqltool.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- CIYAM AT (automated transactions) -->
|
||||
<dependency>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.0</version>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>${ciyam-at.version}</version>
|
||||
</dependency>
|
||||
<!-- UPnP support -->
|
||||
<dependency>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<version>${upnp.version}</version>
|
||||
</dependency>
|
||||
<!-- Bitcoin support -->
|
||||
<dependency>
|
||||
<groupId>org.bitcoinj</groupId>
|
||||
<artifactId>bitcoinj-core</artifactId>
|
||||
<version>${bitcoin.version}</version>
|
||||
<version>${bitcoinj.version}</version>
|
||||
</dependency>
|
||||
<!-- For Litecoin, etc. support, requires bitcoinj -->
|
||||
<dependency>
|
||||
<groupId>com.github.qortal</groupId>
|
||||
<artifactId>altcoinj</artifactId>
|
||||
<version>${altcoinj.version}</version>
|
||||
</dependency>
|
||||
<!-- Utilities -->
|
||||
<dependency>
|
||||
@@ -416,11 +456,36 @@
|
||||
<artifactId>json-simple</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20210307</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>${commons-text.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>${commons-compress.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.tukaani</groupId>
|
||||
<artifactId>xz</artifactId>
|
||||
<version>${xz.version}</version>
|
||||
</dependency>
|
||||
<!-- For bitset/bitmap compression -->
|
||||
<dependency>
|
||||
<groupId>io.druid</groupId>
|
||||
@@ -446,6 +511,10 @@
|
||||
<groupId>org.asynchttpclient</groupId>
|
||||
<artifactId>async-http-client</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>io.druid</groupId>
|
||||
<artifactId>java-util</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- For NTP -->
|
||||
@@ -499,6 +568,23 @@
|
||||
<artifactId>mail</artifactId>
|
||||
<version>1.5.0-b01</version>
|
||||
</dependency>
|
||||
<!-- Unicode homoglyph utilities -->
|
||||
<dependency>
|
||||
<groupId>net.codebox</groupId>
|
||||
<artifactId>homoglyph</artifactId>
|
||||
<version>${homoglyph.version}</version>
|
||||
</dependency>
|
||||
<!-- Unicode support -->
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>${icu4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j-charset</artifactId>
|
||||
<version>${icu4j.version}</version>
|
||||
</dependency>
|
||||
<!-- Jetty -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
@@ -527,6 +613,12 @@
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<!-- Websocket support -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>javax-websocket-server-impl</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<!-- Jersey -->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
@@ -605,5 +697,35 @@
|
||||
<artifactId>bctls-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.java-diff-utils</groupId>
|
||||
<artifactId>java-diff-utils</artifactId>
|
||||
<version>${java-diff-utils.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
34
run.sh
34
run.sh
@@ -1,34 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# There's no need to run as root, so don't allow it, for security reasons
|
||||
if [ "$USER" = "root" ]; then
|
||||
echo "Please su to non-root user before running"
|
||||
exit
|
||||
fi
|
||||
|
||||
# No qortal.jar but we have a Maven built one?
|
||||
# Be helpful and copy across to correct location
|
||||
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then
|
||||
echo "Copying Maven-built Qortal JAR to correct pathname"
|
||||
cp target/qortal*.jar qortal.jar
|
||||
fi
|
||||
|
||||
# Limits Java JVM stack size and maximum heap usage.
|
||||
# Comment out for bigger systems, e.g. non-routers
|
||||
# or when API documentation is enabled
|
||||
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
|
||||
|
||||
# Although java.net.preferIPv4Stack is supposed to be false
|
||||
# by default in Java 11, on some platforms (e.g. FreeBSD 12),
|
||||
# it is overriden to be true by default. Hence we explicitly
|
||||
# set it to true to obtain desired behaviour.
|
||||
nohup nice -n 20 java \
|
||||
-Djava.net.preferIPv4Stack=false \
|
||||
-XX:NativeMemoryTracking=summary \
|
||||
${JVM_MEMORY_ARGS} \
|
||||
-jar qortal.jar \
|
||||
1>run.log 2>&1 &
|
||||
|
||||
# Save backgrounded process's PID
|
||||
echo $! > run.pid
|
||||
echo qortal running as pid $!
|
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
File diff suppressed because it is too large
Load Diff
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
File diff suppressed because it is too large
Load Diff
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
File diff suppressed because it is too large
Load Diff
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* LiteWalletJni code based on https://github.com/PirateNetwork/cordova-plugin-litewallet
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Zero Currency Coin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.rust.litewalletjni;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class LiteWalletJni {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(LiteWalletJni.class);
|
||||
|
||||
public static native String initlogging();
|
||||
public static native String initnew(final String serveruri, final String params, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String initfromseed(final String serveruri, final String params, final String seed, final String birthday, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String initfromb64(final String serveruri, final String params, final String datab64, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String save();
|
||||
|
||||
public static native String execute(final String cmd, final String args);
|
||||
public static native String getseedphrase();
|
||||
public static native String getseedphrasefromentropyb64(final String entropy64);
|
||||
public static native String checkseedphrase(final String input);
|
||||
|
||||
|
||||
private static boolean loaded = false;
|
||||
|
||||
public static void loadLibrary() {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
|
||||
LOGGER.info("OS Name: {}", osName);
|
||||
LOGGER.info("OS Architecture: {}", osArchitecture);
|
||||
|
||||
try {
|
||||
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||
if (libFileName == null) {
|
||||
LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture);
|
||||
return;
|
||||
}
|
||||
|
||||
Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName);
|
||||
System.load(libPath.toAbsolutePath().toString());
|
||||
loaded = true;
|
||||
}
|
||||
catch (UnsatisfiedLinkError e) {
|
||||
LOGGER.info("Unable to load library");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
}
|
@@ -21,18 +21,28 @@ public class HSQLDBPool extends JDBCPool {
|
||||
public Connection tryConnection() throws SQLException {
|
||||
for (int i = 0; i < states.length(); i++) {
|
||||
if (states.compareAndSet(i, RefState.available, RefState.allocated)) {
|
||||
return connections[i].getConnection();
|
||||
JDBCPooledConnection pooledConnection = connections[i];
|
||||
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
}
|
||||
|
||||
if (states.compareAndSet(i, RefState.empty, RefState.allocated)) {
|
||||
try {
|
||||
JDBCPooledConnection connection = (JDBCPooledConnection) source.getPooledConnection();
|
||||
JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection();
|
||||
|
||||
connection.addConnectionEventListener(this);
|
||||
connection.addStatementEventListener(this);
|
||||
connections[i] = connection;
|
||||
if (pooledConnection == null)
|
||||
// Probably shutdown situation
|
||||
return null;
|
||||
|
||||
return connections[i].getConnection();
|
||||
pooledConnection.addConnectionEventListener(this);
|
||||
pooledConnection.addStatementEventListener(this);
|
||||
connections[i] = pooledConnection;
|
||||
|
||||
return pooledConnection.getConnection();
|
||||
} catch (SQLException e) {
|
||||
states.set(i, RefState.empty);
|
||||
}
|
||||
|
@@ -7,18 +7,20 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.Security;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiKey;
|
||||
import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
|
||||
|
||||
public class ApplyUpdate {
|
||||
|
||||
static {
|
||||
@@ -35,9 +37,11 @@ public class ApplyUpdate {
|
||||
private static final String JAR_FILENAME = AutoUpdate.JAR_FILENAME;
|
||||
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
|
||||
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
|
||||
|
||||
private static final long CHECK_INTERVAL = 5 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 5;
|
||||
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 12;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
@@ -65,17 +69,45 @@ public class ApplyUpdate {
|
||||
}
|
||||
|
||||
private static boolean shutdownNode() {
|
||||
String BASE_URI = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(String.format("Shutting down node using API via %s", BASE_URI));
|
||||
String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri));
|
||||
|
||||
// The /admin/stop endpoint requires an API key, which may or may not be already generated
|
||||
boolean apiKeyNewlyGenerated = false;
|
||||
ApiKey apiKey = null;
|
||||
try {
|
||||
apiKey = new ApiKey();
|
||||
if (!apiKey.generated()) {
|
||||
apiKey.generate();
|
||||
apiKeyNewlyGenerated = true;
|
||||
LOGGER.info("Generated API key");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading API key: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Create GET params
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (apiKey != null) {
|
||||
params.put("apiKey", apiKey.toString());
|
||||
}
|
||||
|
||||
// Attempt to stop the node
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
LOGGER.info(String.format("Attempt #%d out of %d to shutdown node", attempt + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(BASE_URI + "admin/stop", null);
|
||||
if (response == null)
|
||||
break;
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(baseUri + "admin/stop", params);
|
||||
if (response == null) {
|
||||
// No response - consider node shut down
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for this auto update, so we need to remove it
|
||||
ApplyUpdate.removeGeneratedApiKey();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.info(String.format("Response from API: %s", response));
|
||||
LOGGER.info(() -> String.format("Response from API: %s", response));
|
||||
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
@@ -85,6 +117,11 @@ public class ApplyUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
if (apiKeyNewlyGenerated) {
|
||||
// API key was newly generated for this auto update, so we need to remove it
|
||||
ApplyUpdate.removeGeneratedApiKey();
|
||||
}
|
||||
|
||||
if (attempt == MAX_ATTEMPTS) {
|
||||
LOGGER.error("Failed to shutdown node - giving up");
|
||||
return false;
|
||||
@@ -93,25 +130,39 @@ public class ApplyUpdate {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void removeGeneratedApiKey() {
|
||||
try {
|
||||
LOGGER.info("Removing newly generated API key...");
|
||||
|
||||
// Delete the API key since it was only generated for this auto update
|
||||
ApiKey apiKey = new ApiKey();
|
||||
apiKey.delete();
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error loading or deleting API key: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void replaceJar() {
|
||||
// Assuming current working directory contains the JAR files
|
||||
Path realJar = Paths.get(JAR_FILENAME);
|
||||
Path newJar = Paths.get(NEW_JAR_FILENAME);
|
||||
|
||||
if (!Files.exists(newJar)) {
|
||||
LOGGER.warn(String.format("Replacement JAR '%s' not found?", newJar));
|
||||
LOGGER.warn(() -> String.format("Replacement JAR '%s' not found?", newJar));
|
||||
return;
|
||||
}
|
||||
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
LOGGER.info(String.format("Attempt #%d out of %d to replace JAR", attempt + 1, MAX_ATTEMPTS));
|
||||
final int attemptForLogging = attempt;
|
||||
LOGGER.info(() -> String.format("Attempt #%d out of %d to replace JAR", attemptForLogging + 1, MAX_ATTEMPTS));
|
||||
|
||||
try {
|
||||
Files.copy(newJar, realJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.info(String.format("Unable to replace JAR: %s", e.getMessage()));
|
||||
LOGGER.info(() -> String.format("Unable to replace JAR: %s", e.getMessage()));
|
||||
|
||||
// Try again
|
||||
}
|
||||
@@ -119,6 +170,7 @@ public class ApplyUpdate {
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.warn("Ignoring interrupt...");
|
||||
// Doggedly retry
|
||||
}
|
||||
}
|
||||
@@ -129,13 +181,13 @@ public class ApplyUpdate {
|
||||
|
||||
private static void restartNode(String[] args) {
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.info(String.format("Java home: %s", javaHome));
|
||||
LOGGER.info(() -> String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.info(String.format("Java binary: %s", javaBinary));
|
||||
LOGGER.info(() -> String.format("Java binary: %s", javaBinary));
|
||||
|
||||
Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER);
|
||||
LOGGER.info(String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher));
|
||||
|
||||
List<String> javaCmd;
|
||||
if (Files.exists(exeLauncher)) {
|
||||
@@ -148,6 +200,11 @@ public class ApplyUpdate {
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Reapply any retained, but disabled, -agentlib JVM arg
|
||||
javaCmd = javaCmd.stream()
|
||||
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Call mainClass in JAR
|
||||
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
||||
|
||||
@@ -158,8 +215,22 @@ public class ApplyUpdate {
|
||||
try {
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
} catch (IOException e) {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
if (Files.exists(exeLauncher)) {
|
||||
LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE));
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
// New process will inherit our stdout and stderr
|
||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||
process.getOutputStream().close();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
76
src/main/java/org/qortal/RepositoryMaintenance.java
Normal file
76
src/main/java/org/qortal/RepositoryMaintenance.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package org.qortal;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class RepositoryMaintenance {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(RepositoryMaintenance.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
LOGGER.info("Repository maintenance starting up...");
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
try {
|
||||
if (args.length > 0)
|
||||
Settings.fileInstance(args[0]);
|
||||
else
|
||||
Settings.getInstance();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Settings file error: " + t.getMessage());
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
LOGGER.info("Opening repository");
|
||||
try {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
} catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
if (e.getCause() == null) {
|
||||
LOGGER.info("Repository in use by another process?");
|
||||
} else {
|
||||
LOGGER.error("Unable to start repository", e);
|
||||
}
|
||||
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
LOGGER.info("Starting repository periodic maintenance. This can take a while...");
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.performPeriodicMaintenance(null);
|
||||
|
||||
LOGGER.info("Repository periodic maintenance completed");
|
||||
} catch (DataException | TimeoutException e) {
|
||||
LOGGER.error("Repository periodic maintenance failed", e);
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info("Shutting down repository");
|
||||
RepositoryManager.closeRepositoryFactory();
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Error occurred while shutting down repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,20 +1,23 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import static org.qortal.utils.Amounts.prettyAmount;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
|
||||
public class Account {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Account.class);
|
||||
@@ -51,39 +54,62 @@ public class Account {
|
||||
return new AccountData(this.address);
|
||||
}
|
||||
|
||||
public void ensureAccount() throws DataException {
|
||||
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
|
||||
}
|
||||
|
||||
// Balance manipulations - assetId is 0 for QORT
|
||||
|
||||
public BigDecimal getBalance(long assetId, int height) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId, height);
|
||||
public long getConfirmedBalance(long assetId) throws DataException {
|
||||
AccountBalanceData accountBalanceData;
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes request data from peers instead of the local db
|
||||
accountBalanceData = LiteNode.getInstance().fetchAccountBalance(this.address, assetId);
|
||||
}
|
||||
else {
|
||||
// All other node types fetch from the local db
|
||||
accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
}
|
||||
|
||||
if (accountBalanceData == null)
|
||||
return BigDecimal.ZERO.setScale(8);
|
||||
return 0;
|
||||
|
||||
return accountBalanceData.getBalance();
|
||||
}
|
||||
|
||||
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
if (accountBalanceData == null)
|
||||
return BigDecimal.ZERO.setScale(8);
|
||||
|
||||
return accountBalanceData.getBalance();
|
||||
}
|
||||
|
||||
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
|
||||
public void setConfirmedBalance(long assetId, long balance) throws DataException {
|
||||
// Safety feature!
|
||||
if (balance.compareTo(BigDecimal.ZERO) < 0) {
|
||||
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", balance.toPlainString(), assetId, this.address);
|
||||
if (balance < 0) {
|
||||
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", prettyAmount(balance), assetId, this.address);
|
||||
LOGGER.error(message);
|
||||
throw new DataException(message);
|
||||
}
|
||||
|
||||
// Delete account balance record instead of setting balance to zero
|
||||
if (balance == 0) {
|
||||
this.repository.getAccountRepository().delete(this.address, assetId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't have a balance without an account - make sure it exists!
|
||||
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
|
||||
this.ensureAccount();
|
||||
|
||||
AccountBalanceData accountBalanceData = new AccountBalanceData(this.address, assetId, balance);
|
||||
this.repository.getAccountRepository().save(accountBalanceData);
|
||||
|
||||
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, balance.toPlainString(), assetId));
|
||||
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, prettyAmount(balance), assetId));
|
||||
}
|
||||
|
||||
// Convenience method
|
||||
public void modifyAssetBalance(long assetId, long deltaBalance) throws DataException {
|
||||
this.repository.getAccountRepository().modifyAssetBalance(this.getAddress(), assetId, deltaBalance);
|
||||
|
||||
LOGGER.trace(() -> String.format("%s balance %s by %s [assetId %s]",
|
||||
this.address,
|
||||
(deltaBalance >= 0 ? "increased" : "decreased"),
|
||||
prettyAmount(Math.abs(deltaBalance)),
|
||||
assetId));
|
||||
}
|
||||
|
||||
public void deleteBalance(long assetId) throws DataException {
|
||||
@@ -99,38 +125,11 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] getLastReference() throws DataException {
|
||||
byte[] reference = this.repository.getAccountRepository().getLastReference(this.address);
|
||||
byte[] reference = AccountRefCache.getLastReference(this.repository, this.address);
|
||||
LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference)));
|
||||
return reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
|
||||
* <p>
|
||||
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
|
||||
* repository changes.
|
||||
*
|
||||
* @return byte[] reference, or null if no unconfirmed transactions for this account.
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] getUnconfirmedLastReference() throws DataException {
|
||||
// Newest unconfirmed transaction takes priority
|
||||
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
|
||||
|
||||
byte[] reference = null;
|
||||
|
||||
for (TransactionData transactionData : unconfirmedTransactions) {
|
||||
String unconfirmedTransactionAddress = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
|
||||
|
||||
if (unconfirmedTransactionAddress.equals(this.address))
|
||||
reference = transactionData.getSignature();
|
||||
}
|
||||
|
||||
final byte[] loggingReference = reference;
|
||||
LOGGER.trace(() -> String.format("Last unconfirmed reference for %s is %s", this.address, loggingReference == null ? "null" : Base58.encode(loggingReference)));
|
||||
return reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last reference for account.
|
||||
*
|
||||
@@ -143,7 +142,7 @@ public class Account {
|
||||
|
||||
AccountData accountData = this.buildAccountData();
|
||||
accountData.setReference(reference);
|
||||
this.repository.getAccountRepository().setLastReference(accountData);
|
||||
AccountRefCache.setLastReference(this.repository, accountData);
|
||||
}
|
||||
|
||||
// Default groupID manipulations
|
||||
@@ -204,16 +203,26 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public boolean canMint() throws DataException {
|
||||
Integer level = this.getLevel();
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return false;
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||
return true;
|
||||
|
||||
if (this.isFounder())
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns account's blockMinted (0+) or null if account not found in repository. */
|
||||
public Integer getBlocksMinted() throws DataException {
|
||||
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
||||
}
|
||||
|
||||
|
||||
/** Returns whether account can build reward-shares.
|
||||
* <p>
|
||||
* To be able to create reward-shares, the account needs to pass at least one of these tests:<br>
|
||||
@@ -226,11 +235,15 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public boolean canRewardShare() throws DataException {
|
||||
Integer level = this.getLevel();
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return false;
|
||||
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||
return true;
|
||||
|
||||
if (this.isFounder())
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -264,20 +277,20 @@ public class Account {
|
||||
* @throws DataException
|
||||
*/
|
||||
public int getEffectiveMintingLevel() throws DataException {
|
||||
if (this.isFounder())
|
||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||
|
||||
Integer level = this.getLevel();
|
||||
if (level == null)
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
if (accountData == null)
|
||||
return 0;
|
||||
|
||||
return level;
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||
|
||||
return accountData.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
* this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
@@ -290,8 +303,29 @@ public class Account {
|
||||
if (rewardShareData == null)
|
||||
return 0;
|
||||
|
||||
PublicKeyAccount rewardShareMinter = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
/**
|
||||
* Returns 'effective' minting level, with a fix for the zero level.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
* @return 0+
|
||||
* @throws DataException
|
||||
*/
|
||||
public static int getRewardShareEffectiveMintingLevelIncludingLevelZero(Repository repository, byte[] rewardSharePublicKey) throws DataException {
|
||||
// Find actual minter and get their effective minting level
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
|
||||
if (rewardShareData == null)
|
||||
return 0;
|
||||
|
||||
else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
|
||||
return 0;
|
||||
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
}
|
||||
|
217
src/main/java/org/qortal/account/AccountRefCache.java
Normal file
217
src/main/java/org/qortal/account/AccountRefCache.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
/**
|
||||
* Account lastReference caching
|
||||
* <p>
|
||||
* When checking an account's lastReference, the value returned should be the
|
||||
* most recent value set after processing the most recent block.
|
||||
* <p>
|
||||
* However, when processing a batch of transactions, e.g. during block processing or validation,
|
||||
* each transaction needs to check, and maybe update, multiple accounts' lastReference values.
|
||||
* <p>
|
||||
* Because the intermediate updates would affect future checks, we set up a cache of that
|
||||
* maintains a consistent value for fetching lastReference, but also tracks the latest new
|
||||
* value, without the overhead of repository calls.
|
||||
* <p>
|
||||
* Thus, when batch transaction processing is finished, only the latest new lastReference values
|
||||
* can be committed to the repository, via {@link AccountRefCache#commit()}.
|
||||
* <p>
|
||||
* Getting and setting lastReferences values are done the usual way via
|
||||
* {@link Account#getLastReference()} and {@link Account#setLastReference(byte[])} which call
|
||||
* package-visibility methods in <tt>AccountRefCache</tt>.
|
||||
* <p>
|
||||
* If {@link Account#getLastReference()} or {@link Account#setLastReference(byte[])} are called
|
||||
* outside of caching then lastReference values are fetched/set directly from/to the repository.
|
||||
* <p>
|
||||
* <tt>AccountRefCache</tt> implements <tt>AutoCloseable</tt> for (typical) use in a try-with-resources block.
|
||||
*
|
||||
* @see Account#getLastReference()
|
||||
* @see Account#setLastReference(byte[])
|
||||
* @see org.qortal.block.Block#process()
|
||||
*/
|
||||
public class AccountRefCache implements AutoCloseable {
|
||||
|
||||
private static final Map<Repository, RefCache> CACHE = new HashMap<>();
|
||||
|
||||
private static class RefCache {
|
||||
private final Map<String, byte[]> getLastReferenceValues = new HashMap<>();
|
||||
private final Map<String, Pair<byte[], byte[]>> setLastReferenceValues = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Function for merging publicKey from new data with old publicKey from map.
|
||||
* <p>
|
||||
* Last reference is <tt>A</tt> element in pair.<br>
|
||||
* Public key is <tt>B</tt> element in pair.
|
||||
*/
|
||||
private static final BinaryOperator<Pair<byte[], byte[]>> mergePublicKey = (oldPair, newPair) -> {
|
||||
// If passed new pair contains non-null publicKey, then we use that one in preference.
|
||||
if (newPair.getB() == null)
|
||||
// Otherwise, inherit publicKey from old map value.
|
||||
newPair.setB(oldPair.getB());
|
||||
|
||||
// We always use new lastReference from new pair.
|
||||
return newPair;
|
||||
};
|
||||
|
||||
|
||||
public byte[] getLastReference(Repository repository, String address) throws DataException {
|
||||
synchronized (this.getLastReferenceValues) {
|
||||
byte[] lastReference = getLastReferenceValues.get(address);
|
||||
if (lastReference != null)
|
||||
// address is present in map, lastReference not null
|
||||
return lastReference;
|
||||
|
||||
// address is present in map, just lastReference is null
|
||||
if (getLastReferenceValues.containsKey(address))
|
||||
return null;
|
||||
|
||||
lastReference = repository.getAccountRepository().getLastReference(address);
|
||||
this.getLastReferenceValues.put(address, lastReference);
|
||||
return lastReference;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLastReference(AccountData accountData) {
|
||||
// We're only interested in lastReference and publicKey
|
||||
Pair<byte[], byte[]> newPair = new Pair<>(accountData.getReference(), accountData.getPublicKey());
|
||||
|
||||
synchronized (this.setLastReferenceValues) {
|
||||
setLastReferenceValues.merge(accountData.getAddress(), newPair, mergePublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Pair<byte[], byte[]>> getNewLastReferences() {
|
||||
return setLastReferenceValues;
|
||||
}
|
||||
}
|
||||
|
||||
private Repository repository;
|
||||
|
||||
/**
|
||||
* Constructs a new account reference cache, unique to passed <tt>repository</tt> handle.
|
||||
*
|
||||
* @param repository
|
||||
* @throws IllegalStateException if a cache already exists for <tt>repository</tt>
|
||||
*/
|
||||
public AccountRefCache(Repository repository) {
|
||||
RefCache refCache = new RefCache();
|
||||
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.putIfAbsent(repository, refCache) != null)
|
||||
throw new IllegalStateException("Account reference cache entry already exists");
|
||||
}
|
||||
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all cached setLastReference account-reference values into repository.
|
||||
* <p>
|
||||
* Closes cache to prevent any future setLastReference() attempts post-commit.
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public void commit() throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
// Also duplicated in close(), this prevents future setLastReference() attempts post-commit.
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.remove(this.repository);
|
||||
}
|
||||
|
||||
if (refCache == null)
|
||||
throw new IllegalStateException("Tried to commit non-existent account reference cache");
|
||||
|
||||
Map<String, Pair<byte[], byte[]>> newLastReferenceValues = refCache.getNewLastReferences();
|
||||
|
||||
for (Entry<String, Pair<byte[], byte[]>> entry : newLastReferenceValues.entrySet()) {
|
||||
AccountData accountData = new AccountData(entry.getKey());
|
||||
|
||||
accountData.setReference(entry.getValue().getA());
|
||||
|
||||
if (entry.getValue().getB() != null)
|
||||
accountData.setPublicKey(entry.getValue().getB());
|
||||
|
||||
this.repository.getAccountRepository().setLastReference(accountData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (CACHE) {
|
||||
CACHE.remove(this.repository);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns lastReference value for account.
|
||||
* <p>
|
||||
* If cache is not in effect for passed <tt>repository</tt> handle,
|
||||
* then this method fetches lastReference directly from repository.
|
||||
* <p>
|
||||
* If cache <i>is</i> in effect, then this method returns cached
|
||||
* lastReference, which is <b>not</b> affected by calls to
|
||||
* <tt>setLastReference</tt>.
|
||||
* <p>
|
||||
* Typically called by corresponding method in Account class.
|
||||
*
|
||||
* @param repository
|
||||
* @param address account's address
|
||||
* @return account's lastReference, or null if account unknown, or lastReference not set
|
||||
* @throws DataException
|
||||
*/
|
||||
/*package*/ static byte[] getLastReference(Repository repository, String address) throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.get(repository);
|
||||
}
|
||||
|
||||
if (refCache == null)
|
||||
return repository.getAccountRepository().getLastReference(address);
|
||||
|
||||
return refCache.getLastReference(repository, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lastReference value for account.
|
||||
* <p>
|
||||
* If cache is not in effect for passed <tt>repository</tt> handle,
|
||||
* then this method sets lastReference directly in repository.
|
||||
* <p>
|
||||
* If cache <i>is</i> in effect, then this method caches the new
|
||||
* lastReference, which is <b>not</b> returned by calls to
|
||||
* <tt>getLastReference</tt>.
|
||||
* <p>
|
||||
* Typically called by corresponding method in Account class.
|
||||
*
|
||||
* @param repository
|
||||
* @param accountData
|
||||
* @throws DataException
|
||||
*/
|
||||
/*package*/ static void setLastReference(Repository repository, AccountData accountData) throws DataException {
|
||||
RefCache refCache;
|
||||
|
||||
synchronized (CACHE) {
|
||||
refCache = CACHE.get(repository);
|
||||
}
|
||||
|
||||
if (refCache == null) {
|
||||
repository.getAccountRepository().setLastReference(accountData);
|
||||
return;
|
||||
}
|
||||
|
||||
refCache.setLastReference(accountData);
|
||||
}
|
||||
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public final class GenesisAccount extends PublicKeyAccount {
|
||||
|
||||
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
public GenesisAccount(Repository repository) {
|
||||
super(repository, PUBLIC_KEY);
|
||||
}
|
||||
|
||||
}
|
24
src/main/java/org/qortal/account/NullAccount.java
Normal file
24
src/main/java/org/qortal/account/NullAccount.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public final class NullAccount extends PublicKeyAccount {
|
||||
|
||||
public static final byte[] PUBLIC_KEY = new byte[32];
|
||||
public static final String ADDRESS = Crypto.toAddress(PUBLIC_KEY);
|
||||
|
||||
public NullAccount(Repository repository) {
|
||||
super(repository, PUBLIC_KEY, ADDRESS);
|
||||
}
|
||||
|
||||
protected NullAccount() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(byte[] signature, byte[] message) {
|
||||
// Can't sign, hence can't verify.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -2,31 +2,24 @@ package org.qortal.account;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.qortal.crypto.BouncyCastle25519;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = 64;
|
||||
private static final int SHARED_SECRET_LENGTH = 32;
|
||||
|
||||
private final byte[] privateKey;
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
|
||||
|
||||
/**
|
||||
* Create PrivateKeyAccount using byte[32] seed.
|
||||
* Create PrivateKeyAccount using byte[32] private key.
|
||||
*
|
||||
* @param seed
|
||||
* @param privateKey
|
||||
* byte[32] used to create private/public key pair
|
||||
* @throws IllegalArgumentException
|
||||
* if passed invalid seed
|
||||
* if passed invalid privateKey
|
||||
*/
|
||||
public PrivateKeyAccount(Repository repository, byte[] seed) {
|
||||
this(repository, new Ed25519PrivateKeyParameters(seed, 0));
|
||||
public PrivateKeyAccount(Repository repository, byte[] privateKey) {
|
||||
this(repository, new Ed25519PrivateKeyParameters(privateKey, 0));
|
||||
}
|
||||
|
||||
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
|
||||
@@ -44,29 +37,12 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
public static byte[] toPublicKey(byte[] seed) {
|
||||
return new Ed25519PrivateKeyParameters(seed, 0).generatePublicKey().getEncoded();
|
||||
}
|
||||
|
||||
public byte[] sign(byte[] message) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
this.edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPublicKeyParams, null, message, 0, message.length, signature, 0);
|
||||
|
||||
return signature;
|
||||
return Crypto.sign(this.edPrivateKeyParams, message);
|
||||
}
|
||||
|
||||
public byte[] getSharedSecret(byte[] publicKey) {
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(this.privateKey);
|
||||
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
|
||||
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
||||
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
|
||||
|
||||
return sharedSecret;
|
||||
return Crypto.getSharedSecret(this.privateKey, publicKey);
|
||||
}
|
||||
|
||||
public byte[] getRewardSharePrivateKey(byte[] publicKey) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -22,6 +21,18 @@ public class PublicKeyAccount extends Account {
|
||||
this.publicKey = edPublicKeyParams.getEncoded();
|
||||
}
|
||||
|
||||
protected PublicKeyAccount(Repository repository, byte[] publicKey, String address) {
|
||||
super(repository, address);
|
||||
|
||||
this.publicKey = publicKey;
|
||||
this.edPublicKeyParams = null;
|
||||
}
|
||||
|
||||
protected PublicKeyAccount() {
|
||||
this.publicKey = null;
|
||||
this.edPublicKeyParams = null;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
@@ -34,15 +45,7 @@ public class PublicKeyAccount extends Account {
|
||||
}
|
||||
|
||||
public boolean verify(byte[] signature, byte[] message) {
|
||||
return PublicKeyAccount.verify(this.publicKey, signature, message);
|
||||
}
|
||||
|
||||
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
try {
|
||||
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
return Crypto.verify(this.publicKey, signature, message);
|
||||
}
|
||||
|
||||
public static String getAddress(byte[] publicKey) {
|
||||
|
27
src/main/java/org/qortal/api/AmountTypeAdapter.java
Normal file
27
src/main/java/org/qortal/api/AmountTypeAdapter.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
public class AmountTypeAdapter extends XmlAdapter<String, Long> {
|
||||
|
||||
@Override
|
||||
public Long unmarshal(String input) throws Exception {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
return new BigDecimal(input).setScale(8).unscaledValue().longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Long output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
return Amounts.prettyAmount(output);
|
||||
}
|
||||
|
||||
}
|
@@ -5,42 +5,49 @@ import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE)
|
||||
@XmlRootElement
|
||||
public enum ApiError {
|
||||
// COMMON
|
||||
UNKNOWN(0, 500),
|
||||
// UNKNOWN(0, 500),
|
||||
JSON(1, 400),
|
||||
NO_BALANCE(2, 422),
|
||||
NOT_YET_RELEASED(3, 422),
|
||||
INSUFFICIENT_BALANCE(2, 402),
|
||||
// NOT_YET_RELEASED(3, 422),
|
||||
UNAUTHORIZED(4, 403),
|
||||
REPOSITORY_ISSUE(5, 500),
|
||||
NON_PRODUCTION(6, 403),
|
||||
BLOCKCHAIN_NEEDS_SYNC(7, 503),
|
||||
NO_TIME_SYNC(8, 503),
|
||||
|
||||
// VALIDATION
|
||||
INVALID_SIGNATURE(101, 400),
|
||||
INVALID_ADDRESS(102, 400),
|
||||
INVALID_SEED(103, 400),
|
||||
INVALID_AMOUNT(104, 400),
|
||||
INVALID_FEE(105, 400),
|
||||
INVALID_SENDER(106, 400),
|
||||
INVALID_RECIPIENT(107, 400),
|
||||
INVALID_NAME_LENGTH(108, 400),
|
||||
INVALID_VALUE_LENGTH(109, 400),
|
||||
INVALID_NAME_OWNER(110, 400),
|
||||
INVALID_BUYER(111, 400),
|
||||
// INVALID_SEED(103, 400),
|
||||
// INVALID_AMOUNT(104, 400),
|
||||
// INVALID_FEE(105, 400),
|
||||
// INVALID_SENDER(106, 400),
|
||||
// INVALID_RECIPIENT(107, 400),
|
||||
// INVALID_NAME_LENGTH(108, 400),
|
||||
// INVALID_VALUE_LENGTH(109, 400),
|
||||
// INVALID_NAME_OWNER(110, 400),
|
||||
// INVALID_BUYER(111, 400),
|
||||
INVALID_PUBLIC_KEY(112, 400),
|
||||
INVALID_OPTIONS_LENGTH(113, 400),
|
||||
INVALID_OPTION_LENGTH(114, 400),
|
||||
// INVALID_OPTIONS_LENGTH(113, 400),
|
||||
// INVALID_OPTION_LENGTH(114, 400),
|
||||
INVALID_DATA(115, 400),
|
||||
INVALID_DATA_LENGTH(116, 400),
|
||||
INVALID_UPDATE_VALUE(117, 400),
|
||||
KEY_ALREADY_EXISTS(118, 422),
|
||||
KEY_NOT_EXISTS(119, 404),
|
||||
LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
|
||||
FEE_LESS_REQUIRED(121, 422),
|
||||
WALLET_NOT_IN_SYNC(122, 422),
|
||||
// INVALID_DATA_LENGTH(116, 400),
|
||||
// INVALID_UPDATE_VALUE(117, 400),
|
||||
// KEY_ALREADY_EXISTS(118, 422),
|
||||
// KEY_NOT_EXISTS(119, 404),
|
||||
// LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
|
||||
// FEE_LESS_REQUIRED(121, 422),
|
||||
// WALLET_NOT_IN_SYNC(122, 422),
|
||||
INVALID_NETWORK_ADDRESS(123, 404),
|
||||
ADDRESS_NO_EXISTS(124, 404),
|
||||
ADDRESS_UNKNOWN(124, 404),
|
||||
INVALID_CRITERIA(125, 400),
|
||||
INVALID_REFERENCE(126, 400),
|
||||
TRANSFORMATION_ERROR(127, 400),
|
||||
@@ -49,75 +56,87 @@ public enum ApiError {
|
||||
CANNOT_MINT(130, 400),
|
||||
|
||||
// WALLET
|
||||
WALLET_NO_EXISTS(201, 404),
|
||||
WALLET_ADDRESS_NO_EXISTS(202, 404),
|
||||
WALLET_LOCKED(203, 422),
|
||||
WALLET_ALREADY_EXISTS(204, 422),
|
||||
WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
|
||||
// WALLET_NO_EXISTS(201, 404),
|
||||
// WALLET_ADDRESS_NO_EXISTS(202, 404),
|
||||
// WALLET_LOCKED(203, 422),
|
||||
// WALLET_ALREADY_EXISTS(204, 422),
|
||||
// WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
|
||||
|
||||
// BLOCKS
|
||||
BLOCK_NO_EXISTS(301, 404),
|
||||
BLOCK_UNKNOWN(301, 404),
|
||||
|
||||
// TRANSACTIONS
|
||||
TRANSACTION_NO_EXISTS(311, 404),
|
||||
TRANSACTION_UNKNOWN(311, 404),
|
||||
PUBLIC_KEY_NOT_FOUND(304, 404),
|
||||
TRANSACTION_INVALID(312, 400),
|
||||
|
||||
// NAMING
|
||||
NAME_NO_EXISTS(401, 404),
|
||||
NAME_ALREADY_EXISTS(402, 422),
|
||||
NAME_ALREADY_FOR_SALE(403, 422),
|
||||
NAME_NOT_LOWER_CASE(404, 422),
|
||||
NAME_SALE_NO_EXISTS(410, 404),
|
||||
BUYER_ALREADY_OWNER(411, 422),
|
||||
NAME_UNKNOWN(401, 404),
|
||||
// NAME_ALREADY_EXISTS(402, 422),
|
||||
// NAME_ALREADY_FOR_SALE(403, 422),
|
||||
// NAME_NOT_LOWER_CASE(404, 422),
|
||||
// NAME_SALE_NO_EXISTS(410, 404),
|
||||
// BUYER_ALREADY_OWNER(411, 422),
|
||||
|
||||
// POLLS
|
||||
POLL_NO_EXISTS(501, 404),
|
||||
POLL_ALREADY_EXISTS(502, 422),
|
||||
DUPLICATE_OPTION(503, 422),
|
||||
POLL_OPTION_NO_EXISTS(504, 404),
|
||||
ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
|
||||
// POLL_NO_EXISTS(501, 404),
|
||||
// POLL_ALREADY_EXISTS(502, 422),
|
||||
// DUPLICATE_OPTION(503, 422),
|
||||
// POLL_OPTION_NO_EXISTS(504, 404),
|
||||
// ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
|
||||
|
||||
// ASSET
|
||||
INVALID_ASSET_ID(601, 400),
|
||||
INVALID_ORDER_ID(602, 400),
|
||||
ORDER_NO_EXISTS(603, 404),
|
||||
ORDER_UNKNOWN(603, 404),
|
||||
|
||||
// NAME PAYMENTS
|
||||
NAME_NOT_REGISTERED(701, 422),
|
||||
NAME_FOR_SALE(702, 422),
|
||||
NAME_WITH_SPACE(703, 422),
|
||||
// NAME_NOT_REGISTERED(701, 422),
|
||||
// NAME_FOR_SALE(702, 422),
|
||||
// NAME_WITH_SPACE(703, 422),
|
||||
|
||||
// ATs
|
||||
INVALID_DESC_LENGTH(801, 400),
|
||||
EMPTY_CODE(802, 400),
|
||||
DATA_SIZE(803, 400),
|
||||
NULL_PAGES(804, 400),
|
||||
INVALID_TYPE_LENGTH(805, 400),
|
||||
INVALID_TAGS_LENGTH(806, 400),
|
||||
INVALID_CREATION_BYTES(809, 400),
|
||||
// INVALID_DESC_LENGTH(801, 400),
|
||||
// EMPTY_CODE(802, 400),
|
||||
// DATA_SIZE(803, 400),
|
||||
// NULL_PAGES(804, 400),
|
||||
// INVALID_TYPE_LENGTH(805, 400),
|
||||
// INVALID_TAGS_LENGTH(806, 400),
|
||||
// INVALID_CREATION_BYTES(809, 400),
|
||||
|
||||
// BLOG/Namestorage
|
||||
BODY_EMPTY(901, 400),
|
||||
BLOG_DISABLED(902, 403),
|
||||
NAME_NOT_OWNER(903, 422),
|
||||
TX_AMOUNT(904, 400),
|
||||
BLOG_ENTRY_NO_EXISTS(905, 404),
|
||||
BLOG_EMPTY(906, 404),
|
||||
POSTID_EMPTY(907, 400),
|
||||
POST_NOT_EXISTING(908, 404),
|
||||
COMMENTING_DISABLED(909, 403),
|
||||
COMMENT_NOT_EXISTING(910, 404),
|
||||
INVALID_COMMENT_OWNER(911, 422),
|
||||
// BODY_EMPTY(901, 400),
|
||||
// BLOG_DISABLED(902, 403),
|
||||
// NAME_NOT_OWNER(903, 422),
|
||||
// TX_AMOUNT(904, 400),
|
||||
// BLOG_ENTRY_NO_EXISTS(905, 404),
|
||||
// BLOG_EMPTY(906, 404),
|
||||
// POSTID_EMPTY(907, 400),
|
||||
// POST_NOT_EXISTING(908, 404),
|
||||
// COMMENTING_DISABLED(909, 403),
|
||||
// COMMENT_NOT_EXISTING(910, 404),
|
||||
// INVALID_COMMENT_OWNER(911, 422),
|
||||
|
||||
// Messages
|
||||
MESSAGE_FORMAT_NOT_HEX(1001, 400),
|
||||
MESSAGE_BLANK(1002, 400),
|
||||
NO_PUBLIC_KEY(1003, 422),
|
||||
MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
// MESSAGE_FORMAT_NOT_HEX(1001, 400),
|
||||
// MESSAGE_BLANK(1002, 400),
|
||||
// NO_PUBLIC_KEY(1003, 422),
|
||||
// MESSAGESIZE_EXCEEDED(1004, 400),
|
||||
|
||||
// Groups
|
||||
GROUP_UNKNOWN(1101, 404);
|
||||
GROUP_UNKNOWN(1101, 404),
|
||||
|
||||
// Foreign blockchain
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE(1201, 500),
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE(1202, 402),
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON(1203, 408),
|
||||
|
||||
// Trade portal
|
||||
ORDER_SIZE_TOO_SMALL(1300, 402),
|
||||
|
||||
// Data
|
||||
FILE_NOT_FOUND(1401, 404),
|
||||
NO_REPLY(1402, 404);
|
||||
|
||||
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
||||
|
||||
@@ -145,4 +164,4 @@ public enum ApiError {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.api;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
if (Settings.getInstance().isApiLoggingEnabled()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
|
20
src/main/java/org/qortal/api/ApiErrorRoot.java
Normal file
20
src/main/java/org/qortal/api/ApiErrorRoot.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
public class ApiErrorRoot {
|
||||
|
||||
private ApiError apiError;
|
||||
|
||||
@XmlJavaTypeAdapter(ApiErrorTypeAdapter.class)
|
||||
@XmlElement(name = "error")
|
||||
public ApiError getApiError() {
|
||||
return this.apiError;
|
||||
}
|
||||
|
||||
public void setApiError(ApiError apiError) {
|
||||
this.apiError = apiError;
|
||||
}
|
||||
|
||||
}
|
32
src/main/java/org/qortal/api/ApiErrorTypeAdapter.java
Normal file
32
src/main/java/org/qortal/api/ApiErrorTypeAdapter.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
public class ApiErrorTypeAdapter extends XmlAdapter<ApiErrorTypeAdapter.AdaptedApiError, ApiError> {
|
||||
|
||||
public static class AdaptedApiError {
|
||||
public int code;
|
||||
public String description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiError unmarshal(AdaptedApiError adaptedInput) throws Exception {
|
||||
if (adaptedInput == null)
|
||||
return null;
|
||||
|
||||
return ApiError.fromCode(adaptedInput.code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdaptedApiError marshal(ApiError output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
AdaptedApiError adaptedOutput = new AdaptedApiError();
|
||||
adaptedOutput.code = output.getCode();
|
||||
adaptedOutput.description = output.name();
|
||||
|
||||
return adaptedOutput;
|
||||
}
|
||||
|
||||
}
|
@@ -16,4 +16,8 @@ public enum ApiExceptionFactory {
|
||||
return createException(request, apiError, null);
|
||||
}
|
||||
|
||||
public ApiException createCustomException(HttpServletRequest request, ApiError apiError, String message) {
|
||||
return new ApiException(apiError.getStatus(), apiError.getCode(), message, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
107
src/main/java/org/qortal/api/ApiKey.java
Normal file
107
src/main/java/org/qortal/api/ApiKey.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class ApiKey {
|
||||
|
||||
private String apiKey;
|
||||
|
||||
public ApiKey() throws IOException {
|
||||
this.load();
|
||||
}
|
||||
|
||||
public void generate() throws IOException {
|
||||
byte[] apiKey = new byte[16];
|
||||
new SecureRandom().nextBytes(apiKey);
|
||||
this.apiKey = Base58.encode(apiKey);
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
|
||||
/* Filesystem */
|
||||
|
||||
private Path getFilePath() {
|
||||
return Paths.get(Settings.getInstance().getApiKeyPath(), "apikey.txt");
|
||||
}
|
||||
|
||||
private boolean load() throws IOException {
|
||||
Path path = this.getFilePath();
|
||||
File apiKeyFile = new File(path.toString());
|
||||
if (!apiKeyFile.exists()) {
|
||||
// Try settings - to allow legacy API keys to be supported
|
||||
return this.loadLegacyApiKey();
|
||||
}
|
||||
|
||||
try {
|
||||
this.apiKey = new String(Files.readAllBytes(path));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IOException(String.format("Couldn't read contents from file %s", path.toString()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean loadLegacyApiKey() {
|
||||
String legacyApiKey = Settings.getInstance().getApiKey();
|
||||
if (legacyApiKey != null && !legacyApiKey.isEmpty()) {
|
||||
this.apiKey = Settings.getInstance().getApiKey();
|
||||
|
||||
try {
|
||||
// Save it to the apikey file
|
||||
this.save();
|
||||
} catch (IOException e) {
|
||||
// Ignore failures as it will be reloaded from settings next time
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
if (this.apiKey == null || this.apiKey.isEmpty()) {
|
||||
throw new IllegalStateException("Unable to save a blank API key");
|
||||
}
|
||||
|
||||
Path filePath = this.getFilePath();
|
||||
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toString()));
|
||||
writer.write(this.apiKey);
|
||||
writer.close();
|
||||
}
|
||||
|
||||
public void delete() throws IOException {
|
||||
this.apiKey = null;
|
||||
|
||||
Path filePath = this.getFilePath();
|
||||
if (Files.exists(filePath)) {
|
||||
Files.delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean generated() {
|
||||
return (this.apiKey != null);
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return this.getFilePath().toFile().exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
}
|
@@ -149,8 +149,8 @@ public class ApiRequest {
|
||||
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||
|
||||
con.setRequestMethod("GET");
|
||||
con.setConnectTimeout(5000);
|
||||
con.setReadTimeout(3000);
|
||||
con.setConnectTimeout(30000);
|
||||
con.setReadTimeout(10000);
|
||||
ApiRequest.setConnectionSSL(con, ipAddress);
|
||||
|
||||
int status = con.getResponseCode();
|
||||
|
@@ -2,15 +2,32 @@ package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.CustomRequestLog;
|
||||
import org.eclipse.jetty.server.DetectorConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.RequestLog;
|
||||
import org.eclipse.jetty.server.RequestLogWriter;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
@@ -18,10 +35,12 @@ import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.api.websocket.*;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@@ -30,6 +49,7 @@ public class ApiService {
|
||||
|
||||
private final ResourceConfig config;
|
||||
private Server server;
|
||||
private ApiKey apiKey;
|
||||
|
||||
private ApiService() {
|
||||
this.config = new ResourceConfig();
|
||||
@@ -50,12 +70,69 @@ public class ApiService {
|
||||
return this.config.getClasses();
|
||||
}
|
||||
|
||||
public void setApiKey(ApiKey apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public ApiKey getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
// Create API server
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
|
||||
this.server = new Server(endpoint);
|
||||
|
||||
// SSL support if requested
|
||||
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||
|
||||
if (keystorePathname != null && keystorePassword != null) {
|
||||
// SSL version
|
||||
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
||||
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
|
||||
this.server = new Server();
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(Settings.getInstance().getApiPort());
|
||||
|
||||
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||
httpConfig.addCustomizer(src);
|
||||
|
||||
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||
@@ -108,10 +185,32 @@ public class ApiService {
|
||||
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
|
||||
context.addServlet(swaggerUIServlet, "/api-documentation/*");
|
||||
|
||||
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect to Swagger UI start page
|
||||
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to Swagger UI start page
|
||||
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect empty path to API docs
|
||||
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
|
||||
} else {
|
||||
// Simple pages that explains that API documentation is disabled
|
||||
ClassLoader loader = this.getClass().getClassLoader();
|
||||
ServletHolder swaggerUIServlet = new ServletHolder("api-docs-disabled", DefaultServlet.class);
|
||||
swaggerUIServlet.setInitParameter("resourceBase", loader.getResource("api-docs-disabled/").toString());
|
||||
swaggerUIServlet.setInitParameter("dirAllowed", "true");
|
||||
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
|
||||
context.addServlet(swaggerUIServlet, "/api-documentation/*");
|
||||
|
||||
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect empty path to API docs
|
||||
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
|
||||
}
|
||||
|
||||
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
|
||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");
|
||||
|
||||
// Deprecated
|
||||
context.addServlet(PresenceWebSocket.class, "/websockets/presence");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
|
@@ -2,7 +2,7 @@ package org.qortal.api;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
public class Base58TypeAdapter extends XmlAdapter<String, byte[]> {
|
||||
|
||||
|
171
src/main/java/org/qortal/api/DomainMapService.java
Normal file
171
src/main/java/org/qortal/api/DomainMapService.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.rewrite.handler.RewritePatternRule;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DomainMapService {
|
||||
|
||||
private static DomainMapService instance;
|
||||
|
||||
private final ResourceConfig config;
|
||||
private Server server;
|
||||
|
||||
private DomainMapService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.domainmap.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
}
|
||||
|
||||
public static DomainMapService getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DomainMapService();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Iterable<Class<?>> getResources() {
|
||||
return this.config.getClasses();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
// Create API server
|
||||
|
||||
// SSL support if requested
|
||||
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||
|
||||
if (keystorePathname != null && keystorePassword != null) {
|
||||
// SSL version
|
||||
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
||||
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
|
||||
this.server = new Server();
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(Settings.getInstance().getDomainMapPort());
|
||||
|
||||
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||
httpConfig.addCustomizer(src);
|
||||
|
||||
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getDomainMapPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDomainMapPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||
this.server.setErrorHandler(errorHandler);
|
||||
|
||||
// Request logging
|
||||
if (Settings.getInstance().isDomainMapLoggingEnabled()) {
|
||||
RequestLogWriter logWriter = new RequestLogWriter("domainmap-requests.log");
|
||||
logWriter.setAppend(true);
|
||||
logWriter.setTimeZone("UTC");
|
||||
RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
|
||||
this.server.setRequestLog(requestLog);
|
||||
}
|
||||
|
||||
// Access handler (currently no whitelist is used)
|
||||
InetAccessHandler accessHandler = new InetAccessHandler();
|
||||
this.server.setHandler(accessHandler);
|
||||
|
||||
// URL rewriting
|
||||
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||
accessHandler.setHandler(rewriteHandler);
|
||||
|
||||
// Context
|
||||
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
|
||||
context.setContextPath("/");
|
||||
rewriteHandler.setHandler(context);
|
||||
|
||||
// Cross-origin resource sharing
|
||||
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
|
||||
context.addFilter(corsFilterHolder, "/*", null);
|
||||
|
||||
// API servlet
|
||||
ServletContainer container = new ServletContainer(this.config);
|
||||
ServletHolder apiServlet = new ServletHolder(container);
|
||||
apiServlet.setInitOrder(1);
|
||||
context.addServlet(apiServlet, "/*");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
// Failed to start
|
||||
throw new RuntimeException("Failed to start API", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
// Stop server
|
||||
this.server.stop();
|
||||
} catch (Exception e) {
|
||||
// Failed to stop
|
||||
}
|
||||
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
}
|
170
src/main/java/org/qortal/api/GatewayService.java
Normal file
170
src/main/java/org/qortal/api/GatewayService.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class GatewayService {
|
||||
|
||||
private static GatewayService instance;
|
||||
|
||||
private final ResourceConfig config;
|
||||
private Server server;
|
||||
|
||||
private GatewayService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.gateway.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
}
|
||||
|
||||
public static GatewayService getInstance() {
|
||||
if (instance == null)
|
||||
instance = new GatewayService();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Iterable<Class<?>> getResources() {
|
||||
return this.config.getClasses();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
try {
|
||||
// Create API server
|
||||
|
||||
// SSL support if requested
|
||||
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
|
||||
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
|
||||
|
||||
if (keystorePathname != null && keystorePassword != null) {
|
||||
// SSL version
|
||||
if (!Files.isReadable(Path.of(keystorePathname)))
|
||||
throw new RuntimeException("Failed to start SSL API due to broken keystore");
|
||||
|
||||
// BouncyCastle-specific SSLContext build
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
|
||||
|
||||
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
|
||||
keyStore.load(keystoreStream, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
|
||||
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setSslContext(sslContext);
|
||||
|
||||
this.server = new Server();
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(Settings.getInstance().getGatewayPort());
|
||||
|
||||
SecureRequestCustomizer src = new SecureRequestCustomizer();
|
||||
httpConfig.addCustomizer(src);
|
||||
|
||||
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
|
||||
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getGatewayPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getGatewayPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
||||
// Error handler
|
||||
ErrorHandler errorHandler = new ApiErrorHandler();
|
||||
this.server.setErrorHandler(errorHandler);
|
||||
|
||||
// Request logging
|
||||
if (Settings.getInstance().isGatewayLoggingEnabled()) {
|
||||
RequestLogWriter logWriter = new RequestLogWriter("gateway-requests.log");
|
||||
logWriter.setAppend(true);
|
||||
logWriter.setTimeZone("UTC");
|
||||
RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
|
||||
this.server.setRequestLog(requestLog);
|
||||
}
|
||||
|
||||
// Access handler (currently no whitelist is used)
|
||||
InetAccessHandler accessHandler = new InetAccessHandler();
|
||||
this.server.setHandler(accessHandler);
|
||||
|
||||
// URL rewriting
|
||||
RewriteHandler rewriteHandler = new RewriteHandler();
|
||||
accessHandler.setHandler(rewriteHandler);
|
||||
|
||||
// Context
|
||||
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
|
||||
context.setContextPath("/");
|
||||
rewriteHandler.setHandler(context);
|
||||
|
||||
// Cross-origin resource sharing
|
||||
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
|
||||
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
|
||||
context.addFilter(corsFilterHolder, "/*", null);
|
||||
|
||||
// API servlet
|
||||
ServletContainer container = new ServletContainer(this.config);
|
||||
ServletHolder apiServlet = new ServletHolder(container);
|
||||
apiServlet.setInitOrder(1);
|
||||
context.addServlet(apiServlet, "/*");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
// Failed to start
|
||||
throw new RuntimeException("Failed to start API", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
// Stop server
|
||||
this.server.stop();
|
||||
} catch (Exception e) {
|
||||
// Failed to stop
|
||||
}
|
||||
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
}
|
51
src/main/java/org/qortal/api/HTMLParser.java
Normal file
51
src/main/java/org/qortal/api/HTMLParser.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
public class HTMLParser {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
|
||||
|
||||
private String linkPrefix;
|
||||
private byte[] data;
|
||||
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data) {
|
||||
String inPathWithoutFilename = inPath.substring(0, inPath.lastIndexOf('/'));
|
||||
this.linkPrefix = usePrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : "";
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void addAdditionalHeaderTags() {
|
||||
String fileContents = new String(data);
|
||||
Document document = Jsoup.parse(fileContents);
|
||||
String baseUrl = this.linkPrefix + "/";
|
||||
Elements head = document.getElementsByTag("head");
|
||||
if (!head.isEmpty()) {
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s\">", baseUrl);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
||||
// Add meta charset tag
|
||||
String metaCharsetElement = "<meta charset=\"UTF-8\">";
|
||||
head.get(0).prepend(metaCharsetElement);
|
||||
|
||||
}
|
||||
String html = document.html();
|
||||
this.data = html.getBytes();
|
||||
}
|
||||
|
||||
public static boolean isHtmlFile(String path) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
|
||||
public class RewardSharePercentTypeAdapter extends XmlAdapter<String, Integer> {
|
||||
|
||||
@Override
|
||||
public Integer unmarshal(String input) throws Exception {
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
return new BigDecimal(input).setScale(2).unscaledValue().intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String marshal(Integer output) throws Exception {
|
||||
if (output == null)
|
||||
return null;
|
||||
|
||||
return String.format("%d.%02d", output / 100, Math.abs(output % 100));
|
||||
}
|
||||
|
||||
}
|
@@ -1,22 +1,111 @@
|
||||
package org.qortal.api;
|
||||
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class Security {
|
||||
public abstract class Security {
|
||||
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
||||
// TODO: replace with proper authentication
|
||||
public static void checkApiCallAllowed(HttpServletRequest request) {
|
||||
InetAddress remoteAddr;
|
||||
// We may want to allow automatic authentication for local requests, if enabled in settings
|
||||
boolean localAuthBypassEnabled = Settings.getInstance().isLocalAuthBypassEnabled();
|
||||
if (localAuthBypassEnabled) {
|
||||
try {
|
||||
InetAddress remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
if (remoteAddr.isLoopbackAddress()) {
|
||||
// Request originates from loopback address, so allow it
|
||||
return;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// Ignore failure, and fallback to API key authentication
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the API key
|
||||
ApiKey apiKey = Security.getApiKey(request);
|
||||
if (!apiKey.generated()) {
|
||||
// Not generated an API key yet, so disallow sensitive API calls
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "API key not generated");
|
||||
}
|
||||
|
||||
// We require an API key to be passed
|
||||
String passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
if (passedApiKey == null) {
|
||||
// Try query string - this is needed to avoid a CORS preflight. See: https://stackoverflow.com/a/43881141
|
||||
passedApiKey = request.getParameter("apiKey");
|
||||
}
|
||||
if (passedApiKey == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Missing 'X-API-KEY' header");
|
||||
}
|
||||
|
||||
// The API keys must match
|
||||
if (!apiKey.toString().equals(passedApiKey)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "API key invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public static void disallowLoopbackRequests(HttpServletRequest request) {
|
||||
try {
|
||||
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
InetAddress remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
if (remoteAddr.isLoopbackAddress()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Local requests not allowed");
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!remoteAddr.isLoopbackAddress())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public static void disallowLoopbackRequestsIfAuthBypassEnabled(HttpServletRequest request) {
|
||||
if (Settings.getInstance().isLocalAuthBypassEnabled()) {
|
||||
try {
|
||||
InetAddress remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
if (remoteAddr.isLoopbackAddress()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Local requests not allowed when localAuthBypassEnabled is enabled in settings");
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void requirePriorAuthorization(HttpServletRequest request, String resourceId, Service service, String identifier) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceId, null, service, identifier);
|
||||
if (!ArbitraryDataRenderManager.getInstance().isAuthorized(resource)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Call /render/authorize first");
|
||||
}
|
||||
}
|
||||
|
||||
public static void requirePriorAuthorizationOrApiKey(HttpServletRequest request, String resourceId, Service service, String identifier) {
|
||||
try {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
} catch (ApiException e) {
|
||||
// API call wasn't allowed, but maybe it was pre-authorized
|
||||
Security.requirePriorAuthorization(request, resourceId, service, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public static ApiKey getApiKey(HttpServletRequest request) {
|
||||
ApiKey apiKey = ApiService.getInstance().getApiKey();
|
||||
if (apiKey == null) {
|
||||
try {
|
||||
apiKey = new ApiKey();
|
||||
} catch (IOException e) {
|
||||
// Couldn't load API key - so we need to treat it as not generated, and therefore unauthorized
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
|
||||
}
|
||||
ApiService.getInstance().setApiKey(apiKey);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,58 @@
|
||||
package org.qortal.api.domainmap.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@Path("/")
|
||||
@Tag(name = "Domain Map")
|
||||
public class DomainMapResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
|
||||
@GET
|
||||
public HttpServletResponse getIndexByDomainMap() {
|
||||
return this.getDomainMap("/");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{path:.*}")
|
||||
public HttpServletResponse getPathByDomainMap(@PathParam("path") String inPath) {
|
||||
return this.getDomainMap(inPath);
|
||||
}
|
||||
|
||||
private HttpServletResponse getDomainMap(String inPath) {
|
||||
Map<String, String> domainMap = Settings.getInstance().getSimpleDomainMap();
|
||||
if (domainMap != null && domainMap.containsKey(request.getServerName())) {
|
||||
// Build synchronously, so that we don't need to make the summary API endpoints available over
|
||||
// the domain map server. This means that there will be no loading screen, but this is potentially
|
||||
// preferred in this situation anyway (e.g. to avoid confusing search engine robots).
|
||||
return this.get(domainMap.get(request.getServerName()), ResourceIdType.NAME, Service.WEBSITE, inPath, null, "", false, false);
|
||||
}
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
package org.qortal.api.gateway.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
|
||||
|
||||
@Path("/")
|
||||
@Tag(name = "Gateway")
|
||||
public class GatewayResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
/**
|
||||
* We need to allow resource status checking (and building) via the gateway, as the node's API port
|
||||
* may not be forwarded and will almost certainly not be authenticated. Since gateways allow for
|
||||
* all resources to be loaded except those that are blocked, there is no need for authentication.
|
||||
*/
|
||||
@GET
|
||||
@Path("/arbitrary/resource/status/{service}/{name}")
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
return this.getStatus(service, name, null, build);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/arbitrary/resource/status/{service}/{name}/{identifier}")
|
||||
public ArbitraryResourceStatus getResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
return this.getStatus(service, name, identifier, build);
|
||||
}
|
||||
|
||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// No need to handle exception, as it will be reflected in the status
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
public HttpServletResponse getRoot() {
|
||||
return ArbitraryDataRenderer.getResponse(response, 200, "");
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("{name}/{path:.*}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathByName(@PathParam("name") String name,
|
||||
@PathParam("path") String inPath) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, inPath, null, "", true, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{name}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByName(@PathParam("name") String name) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, "/", null, "", true, true);
|
||||
}
|
||||
|
||||
|
||||
// Optional /site alternative for backwards support
|
||||
|
||||
@GET
|
||||
@Path("/site/{name}/{path:.*}")
|
||||
public HttpServletResponse getSitePathByName(@PathParam("name") String name,
|
||||
@PathParam("path") String inPath) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, inPath, null, "/site", true, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/site/{name}")
|
||||
public HttpServletResponse getSiteIndexByName(@PathParam("name") String name) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, "/", null, "/site", true, true);
|
||||
}
|
||||
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActivitySummary {
|
||||
|
||||
public int blockCount;
|
||||
public int transactionCount;
|
||||
public int assetsIssued;
|
||||
public int namesRegistered;
|
||||
private int blockCount;
|
||||
private int assetsIssued;
|
||||
private int namesRegistered;
|
||||
|
||||
// Assuming TransactionType values are contiguous so 'length' equals count
|
||||
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
|
||||
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
|
||||
private int totalTransactionCount = 0;
|
||||
|
||||
public ActivitySummary() {
|
||||
// Needed for JAXB
|
||||
}
|
||||
|
||||
public int getBlockCount() {
|
||||
return this.blockCount;
|
||||
}
|
||||
|
||||
public void setBlockCount(int blockCount) {
|
||||
this.blockCount = blockCount;
|
||||
}
|
||||
|
||||
public int getTotalTransactionCount() {
|
||||
return this.totalTransactionCount;
|
||||
}
|
||||
|
||||
public int getAssetsIssued() {
|
||||
return this.assetsIssued;
|
||||
}
|
||||
|
||||
public void setAssetsIssued(int assetsIssued) {
|
||||
this.assetsIssued = assetsIssued;
|
||||
}
|
||||
|
||||
public int getNamesRegistered() {
|
||||
return this.namesRegistered;
|
||||
}
|
||||
|
||||
public void setNamesRegistered(int namesRegistered) {
|
||||
this.namesRegistered = namesRegistered;
|
||||
}
|
||||
|
||||
public Map<TransactionType, Integer> getTransactionCountByType() {
|
||||
return Collections.unmodifiableMap(this.transactionCountByType);
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
|
||||
this.transactionCountByType.put(transactionType, transactionCount);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
public void setTransactionCountByType(Map<TransactionType, Integer> transactionCountByType) {
|
||||
this.transactionCountByType = new EnumMap<>(transactionCountByType);
|
||||
|
||||
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,11 +1,10 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.asset.OrderData;
|
||||
|
||||
@@ -29,12 +28,14 @@ public class AggregatedOrder {
|
||||
}
|
||||
|
||||
@XmlElement(name = "price")
|
||||
public BigDecimal getPrice() {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long getPrice() {
|
||||
return this.orderData.getPrice();
|
||||
}
|
||||
|
||||
@XmlElement(name = "unfulfilled")
|
||||
public BigDecimal getUnfulfilled() {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long getUnfulfilled() {
|
||||
return this.orderData.getAmount();
|
||||
}
|
||||
|
||||
|
23
src/main/java/org/qortal/api/model/BlockMintingInfo.java
Normal file
23
src/main/java/org/qortal/api/model/BlockMintingInfo.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockMintingInfo {
|
||||
|
||||
public byte[] minterPublicKey;
|
||||
public int minterLevel;
|
||||
public int onlineAccountsCount;
|
||||
public BigDecimal maxDistance;
|
||||
public BigInteger keyDistance;
|
||||
public double keyDistanceRatio;
|
||||
public long timestamp;
|
||||
public long timeDelta;
|
||||
|
||||
public BlockMintingInfo() {
|
||||
}
|
||||
|
||||
}
|
@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockMinterSummary {
|
||||
public class BlockSignerSummary {
|
||||
|
||||
// Properties
|
||||
|
||||
@@ -20,22 +20,25 @@ public class BlockMinterSummary {
|
||||
|
||||
// Constructors
|
||||
|
||||
protected BlockMinterSummary() {
|
||||
protected BlockSignerSummary() {
|
||||
}
|
||||
|
||||
/** Constructs BlockMinterSummary in non-reward-share context. */
|
||||
public BlockMinterSummary(byte[] blockMinterPublicKey, int blockCount) {
|
||||
/** Constructs BlockSignerSummary in non-reward-share context. */
|
||||
public BlockSignerSummary(byte[] blockMinterPublicKey, int blockCount) {
|
||||
this.blockCount = blockCount;
|
||||
|
||||
this.mintingAccountPublicKey = blockMinterPublicKey;
|
||||
this.mintingAccount = Crypto.toAddress(this.mintingAccountPublicKey);
|
||||
}
|
||||
|
||||
/** Constructs BlockMinterSummary in reward-share context. */
|
||||
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String recipientAccount) {
|
||||
this(mintingAccountPublicKey, blockCount);
|
||||
|
||||
/** Constructs BlockSignerSummary in reward-share context. */
|
||||
public BlockSignerSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
|
||||
this.rewardSharePublicKey = rewardSharePublicKey;
|
||||
this.blockCount = blockCount;
|
||||
|
||||
this.mintingAccountPublicKey = mintingAccountPublicKey;
|
||||
this.mintingAccount = minterAccount;
|
||||
|
||||
this.recipientAccount = recipientAccount;
|
||||
}
|
||||
|
@@ -1,61 +1,74 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.Handshake;
|
||||
import org.qortal.network.Peer;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ConnectedPeer {
|
||||
|
||||
public enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND;
|
||||
}
|
||||
public Direction direction;
|
||||
public Handshake handshakeStatus;
|
||||
public Long lastPing;
|
||||
public Long connectedWhen;
|
||||
public Long peersConnectedWhen;
|
||||
public enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND;
|
||||
}
|
||||
|
||||
public String address;
|
||||
public String version;
|
||||
public Long buildTimestamp;
|
||||
public Direction direction;
|
||||
public Handshake handshakeStatus;
|
||||
public Long lastPing;
|
||||
public Long connectedWhen;
|
||||
public Long peersConnectedWhen;
|
||||
|
||||
public Integer lastHeight;
|
||||
@Schema(example = "base58")
|
||||
public byte[] lastBlockSignature;
|
||||
public Long lastBlockTimestamp;
|
||||
public String address;
|
||||
public String version;
|
||||
|
||||
protected ConnectedPeer() {
|
||||
}
|
||||
public String nodeId;
|
||||
|
||||
public ConnectedPeer(Peer peer) {
|
||||
this.direction = peer.isOutbound() ? Direction.OUTBOUND : Direction.INBOUND;
|
||||
this.handshakeStatus = peer.getHandshakeStatus();
|
||||
this.lastPing = peer.getLastPing();
|
||||
public Integer lastHeight;
|
||||
@Schema(example = "base58")
|
||||
public byte[] lastBlockSignature;
|
||||
public Long lastBlockTimestamp;
|
||||
public UUID connectionId;
|
||||
public String age;
|
||||
|
||||
PeerData peerData = peer.getPeerData();
|
||||
this.connectedWhen = peer.getConnectionTimestamp();
|
||||
this.peersConnectedWhen = peer.getPeersConnectionTimestamp();
|
||||
protected ConnectedPeer() {
|
||||
}
|
||||
|
||||
this.address = peerData.getAddress().toString();
|
||||
if (peer.getVersionMessage() != null) {
|
||||
this.version = peer.getVersionMessage().getVersionString();
|
||||
this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp();
|
||||
}
|
||||
public ConnectedPeer(Peer peer) {
|
||||
this.direction = peer.isOutbound() ? Direction.OUTBOUND : Direction.INBOUND;
|
||||
this.handshakeStatus = peer.getHandshakeStatus();
|
||||
this.lastPing = peer.getLastPing();
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData != null) {
|
||||
this.lastHeight = peerChainTipData.getLastHeight();
|
||||
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
|
||||
}
|
||||
}
|
||||
PeerData peerData = peer.getPeerData();
|
||||
this.connectedWhen = peer.getConnectionTimestamp();
|
||||
this.peersConnectedWhen = peer.getPeersConnectionTimestamp();
|
||||
|
||||
this.address = peerData.getAddress().toString();
|
||||
|
||||
this.version = peer.getPeersVersionString();
|
||||
this.nodeId = peer.getPeersNodeId();
|
||||
this.connectionId = peer.getPeerConnectionId();
|
||||
if (peer.getConnectionEstablishedTime() > 0) {
|
||||
long age = (System.currentTimeMillis() - peer.getConnectionEstablishedTime());
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(age);
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(age) - TimeUnit.MINUTES.toSeconds(minutes);
|
||||
this.age = String.format("%dm %ds", minutes, seconds);
|
||||
} else {
|
||||
this.age = "connecting...";
|
||||
}
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData != null) {
|
||||
this.lastHeight = peerChainTipData.getLastHeight();
|
||||
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,34 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinRedeemRequest {
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
|
||||
public byte[] refundPublicKeyHash;
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cUvGNSnu14q6Hr1X7TESjYVTqBpFjj8GGLGjGdpJwD9NhSQKeYUk")
|
||||
public byte[] redeemPrivateKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||
public byte[] receivingAccountInfo;
|
||||
|
||||
public CrossChainBitcoinRedeemRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinRefundRequest {
|
||||
|
||||
@Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
|
||||
public byte[] refundPrivateKey;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] redeemPublicKeyHash;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||
public BigDecimal bitcoinMinerFee;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||
public byte[] receivingAccountInfo;
|
||||
|
||||
public CrossChainBitcoinRefundRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinTemplateRequest {
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
|
||||
public byte[] refundPublicKeyHash;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] redeemPublicKeyHash;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainBitcoinTemplateRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBitcoinyHTLCStatus {
|
||||
|
||||
@Schema(description = "P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)")
|
||||
public String bitcoinP2shAddress;
|
||||
|
||||
@Schema(description = "P2SH balance")
|
||||
public BigDecimal bitcoinP2shBalance;
|
||||
|
||||
@Schema(description = "Can HTLC redeem yet?")
|
||||
public boolean canRedeem;
|
||||
|
||||
@Schema(description = "Can HTLC refund yet?")
|
||||
public boolean canRefund;
|
||||
|
||||
@Schema(description = "Secret used by HTLC redeemer")
|
||||
public byte[] secret;
|
||||
|
||||
public CrossChainBitcoinyHTLCStatus() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainBuildRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] bitcoinPublicKeyHash;
|
||||
|
||||
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
|
||||
public byte[] hashOfSecretB;
|
||||
|
||||
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
|
||||
public Integer tradeTimeout;
|
||||
|
||||
public CrossChainBuildRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainCancelRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal trade AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainCancelRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainDualSecretRequest {
|
||||
|
||||
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] partnerPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
|
||||
public byte[] secretA;
|
||||
|
||||
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
|
||||
public byte[] secretB;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public CrossChainDualSecretRequest() {
|
||||
}
|
||||
|
||||
}
|
127
src/main/java/org/qortal/api/model/CrossChainOfferSummary.java
Normal file
127
src/main/java/org/qortal/api/model/CrossChainOfferSummary.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainOfferSummary {
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
private String qortalAtAddress;
|
||||
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
private String qortalCreator;
|
||||
|
||||
@Schema(description = "AT creator's ephemeral trading key-pair represented as Qortal address")
|
||||
private String qortalCreatorTradeAddress;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@Schema(description = "Bitcoin amount - DEPRECATED: use foreignAmount")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
@Deprecated
|
||||
private long btcAmount;
|
||||
|
||||
@Schema(description = "Foreign blockchain amount")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long foreignAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
private int tradeTimeout;
|
||||
|
||||
@Schema(description = "Current AT execution mode")
|
||||
private AcctMode mode;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal receiving address")
|
||||
private String partnerQortalReceivingAddress;
|
||||
|
||||
private String foreignBlockchain;
|
||||
|
||||
private String acctName;
|
||||
|
||||
protected CrossChainOfferSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
||||
this.qortalCreator = crossChainTradeData.qortalCreator;
|
||||
this.qortalCreatorTradeAddress = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||
this.btcAmount = this.foreignAmount; // Duplicate for deprecated field
|
||||
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
||||
this.mode = crossChainTradeData.mode;
|
||||
this.timestamp = timestamp;
|
||||
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
|
||||
this.foreignBlockchain = crossChainTradeData.foreignBlockchain;
|
||||
this.acctName = crossChainTradeData.acctName;
|
||||
}
|
||||
|
||||
public String getQortalAtAddress() {
|
||||
return this.qortalAtAddress;
|
||||
}
|
||||
|
||||
public String getQortalCreator() {
|
||||
return this.qortalCreator;
|
||||
}
|
||||
|
||||
public String getQortalCreatorTradeAddress() {
|
||||
return this.qortalCreatorTradeAddress;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
public long getForeignAmount() {
|
||||
return this.foreignAmount;
|
||||
}
|
||||
|
||||
public int getTradeTimeout() {
|
||||
return this.tradeTimeout;
|
||||
}
|
||||
|
||||
public AcctMode getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getPartnerQortalReceivingAddress() {
|
||||
return this.partnerQortalReceivingAddress;
|
||||
}
|
||||
|
||||
public String getForeignBlockchain() {
|
||||
return this.foreignBlockchain;
|
||||
}
|
||||
|
||||
public String getAcctName() {
|
||||
return this.acctName;
|
||||
}
|
||||
|
||||
// For debugging mostly
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s: %s", this.qortalAtAddress, this.mode);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainSecretRequest {
|
||||
|
||||
@Schema(description = "Private key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] partnerPrivateKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Secret (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
|
||||
public byte[] secret;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public CrossChainSecretRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeRequest {
|
||||
|
||||
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] tradePublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
|
||||
public byte[] messageTransactionSignature;
|
||||
|
||||
public CrossChainTradeRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeSummary {
|
||||
|
||||
private long tradeTimestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@Deprecated
|
||||
@Schema(description = "DEPRECATED: use foreignAmount instead")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long foreignAmount;
|
||||
|
||||
private String atAddress;
|
||||
|
||||
private String sellerAddress;
|
||||
|
||||
private String buyerReceivingAddress;
|
||||
|
||||
protected CrossChainTradeSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.tradeTimestamp = timestamp;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||
this.btcAmount = this.foreignAmount;
|
||||
this.sellerAddress = crossChainTradeData.qortalCreator;
|
||||
this.buyerReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
|
||||
this.atAddress = crossChainTradeData.qortalAtAddress;
|
||||
}
|
||||
|
||||
public long getTradeTimestamp() {
|
||||
return this.tradeTimestamp;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
public long getForeignAmount() { return this.foreignAmount; }
|
||||
|
||||
public String getAtAddress() { return this.atAddress; }
|
||||
|
||||
public String getSellerAddress() { return this.sellerAddress; }
|
||||
|
||||
public String getBuyerReceivingAddressAddress() { return this.buyerReceivingAddress; }
|
||||
}
|
18
src/main/java/org/qortal/api/model/ListRequest.java
Normal file
18
src/main/java/org/qortal/api/model/ListRequest.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ListRequest {
|
||||
|
||||
@Schema(description = "A list of items")
|
||||
public List<String> items;
|
||||
|
||||
public ListRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -10,6 +10,9 @@ public class NodeInfo {
|
||||
public long uptime;
|
||||
public String buildVersion;
|
||||
public long buildTimestamp;
|
||||
public String nodeId;
|
||||
public boolean isTestNet;
|
||||
public String type;
|
||||
|
||||
public NodeInfo() {
|
||||
}
|
||||
|
35
src/main/java/org/qortal/api/model/NodeStatus.java
Normal file
35
src/main/java/org/qortal/api/model/NodeStatus.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.network.Network;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class NodeStatus {
|
||||
|
||||
public final boolean isMintingPossible;
|
||||
public final boolean isSynchronizing;
|
||||
|
||||
// Not always present
|
||||
public final Integer syncPercent;
|
||||
|
||||
public final int numberOfConnections;
|
||||
|
||||
public final int height;
|
||||
|
||||
public NodeStatus() {
|
||||
this.isMintingPossible = OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures();
|
||||
|
||||
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();
|
||||
|
||||
this.numberOfConnections = Network.getInstance().getImmutableHandshakedPeers().size();
|
||||
|
||||
this.height = Controller.getInstance().getChainHeight();
|
||||
}
|
||||
|
||||
}
|
15
src/main/java/org/qortal/api/model/PeersSummary.java
Normal file
15
src/main/java/org/qortal/api/model/PeersSummary.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class PeersSummary {
|
||||
|
||||
public int inboundConnections;
|
||||
public int outboundConnections;
|
||||
|
||||
public PeersSummary() {
|
||||
}
|
||||
|
||||
}
|
157
src/main/java/org/qortal/api/model/SimpleForeignTransaction.java
Normal file
157
src/main/java/org/qortal/api/model/SimpleForeignTransaction.java
Normal file
@@ -0,0 +1,157 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SimpleForeignTransaction {
|
||||
|
||||
public static class AddressAmount {
|
||||
public final String address;
|
||||
public final long amount;
|
||||
|
||||
protected AddressAmount() {
|
||||
/* For JAXB */
|
||||
this.address = null;
|
||||
this.amount = 0;
|
||||
}
|
||||
|
||||
public AddressAmount(String address, long amount) {
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private String txHash;
|
||||
private long timestamp;
|
||||
|
||||
private List<AddressAmount> inputs;
|
||||
|
||||
public static class Output {
|
||||
public final List<String> addresses;
|
||||
public final long amount;
|
||||
|
||||
protected Output() {
|
||||
/* For JAXB */
|
||||
this.addresses = null;
|
||||
this.amount = 0;
|
||||
}
|
||||
|
||||
public Output(List<String> addresses, long amount) {
|
||||
this.addresses = addresses;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
private List<Output> outputs;
|
||||
|
||||
private long totalAmount;
|
||||
private long fees;
|
||||
|
||||
private Boolean isSentNotReceived;
|
||||
|
||||
protected SimpleForeignTransaction() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
private SimpleForeignTransaction(Builder builder) {
|
||||
this.txHash = builder.txHash;
|
||||
this.timestamp = builder.timestamp;
|
||||
this.inputs = Collections.unmodifiableList(builder.inputs);
|
||||
this.outputs = Collections.unmodifiableList(builder.outputs);
|
||||
|
||||
Objects.requireNonNull(this.txHash);
|
||||
if (timestamp <= 0)
|
||||
throw new IllegalArgumentException("timestamp must be positive");
|
||||
|
||||
long totalGrossAmount = this.inputs.stream().map(addressAmount -> addressAmount.amount).reduce(0L, Long::sum);
|
||||
this.totalAmount = this.outputs.stream().map(addressAmount -> addressAmount.amount).reduce(0L, Long::sum);
|
||||
|
||||
this.fees = totalGrossAmount - this.totalAmount;
|
||||
|
||||
this.isSentNotReceived = builder.isSentNotReceived;
|
||||
}
|
||||
|
||||
public String getTxHash() {
|
||||
return this.txHash;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public List<AddressAmount> getInputs() {
|
||||
return this.inputs;
|
||||
}
|
||||
|
||||
public List<Output> getOutputs() {
|
||||
return this.outputs;
|
||||
}
|
||||
|
||||
public long getTotalAmount() {
|
||||
return this.totalAmount;
|
||||
}
|
||||
|
||||
public long getFees() {
|
||||
return this.fees;
|
||||
}
|
||||
|
||||
public Boolean isSentNotReceived() {
|
||||
return this.isSentNotReceived;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String txHash;
|
||||
private long timestamp;
|
||||
private List<AddressAmount> inputs = new ArrayList<>();
|
||||
private List<Output> outputs = new ArrayList<>();
|
||||
private Boolean isSentNotReceived;
|
||||
|
||||
public Builder txHash(String txHash) {
|
||||
this.txHash = Objects.requireNonNull(txHash);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder timestamp(long timestamp) {
|
||||
if (timestamp <= 0)
|
||||
throw new IllegalArgumentException("timestamp must be positive");
|
||||
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder input(String address, long amount) {
|
||||
Objects.requireNonNull(address);
|
||||
if (amount < 0)
|
||||
throw new IllegalArgumentException("amount must be zero or positive");
|
||||
|
||||
AddressAmount input = new AddressAmount(address, amount);
|
||||
inputs.add(input);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder output(List<String> addresses, long amount) {
|
||||
Objects.requireNonNull(addresses);
|
||||
if (amount < 0)
|
||||
throw new IllegalArgumentException("amount must be zero or positive");
|
||||
|
||||
Output output = new Output(addresses, amount);
|
||||
outputs.add(output);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isSentNotReceived(Boolean isSentNotReceived) {
|
||||
this.isSentNotReceived = isSentNotReceived;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleForeignTransaction build() {
|
||||
return new SimpleForeignTransaction(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinSendRequest {
|
||||
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of BTC to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 BTC (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public BitcoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DigibyteSendRequest {
|
||||
|
||||
@Schema(description = "Digibyte BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Digibyte address ('legacy' P2PKH only)", example = "1DigByteEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of DGB to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long digibyteAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DGB (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public DigibyteSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DogecoinSendRequest {
|
||||
|
||||
@Schema(description = "Dogecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Dogecoin address ('legacy' P2PKH only)", example = "DoGecoinEaterAddressDontSendhLfzKD")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of DOGE to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long dogecoinAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DOGE (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public DogecoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class LitecoinSendRequest {
|
||||
|
||||
@Schema(description = "Litecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Litecoin address ('legacy' P2PKH only)", example = "LiTecoinEaterAddressDontSendhLfzKD")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of LTC to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long litecoinAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 LTC (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public LitecoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class PirateChainSendRequest {
|
||||
|
||||
@Schema(description = "32 bytes of entropy, Base58 encoded", example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV")
|
||||
public String entropy58;
|
||||
|
||||
@Schema(description = "Recipient's Pirate Chain address", example = "zc...")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of ARRR to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long arrrAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 ARRR (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
@Schema(description = "Optional memo to include information for the recipient", example = "zc...")
|
||||
public String memo;
|
||||
|
||||
public PirateChainSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RavencoinSendRequest {
|
||||
|
||||
@Schema(description = "Ravencoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Ravencoin address ('legacy' P2PKH only)", example = "1RvnCoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of RVN to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long ravencoinAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 RVN (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public RavencoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotCreateRequest {
|
||||
|
||||
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "QORT amount paid out on successful trade", example = "80.40000000", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "80.50000000", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Deprecated
|
||||
@Schema(description = "Bitcoin amount wanted in return. DEPRECATED: use foreignAmount instead", example = "0.00864200", type = "number", hidden = true)
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Foreign blockchain. Note: default (BITCOIN) to be removed in the future", example = "BITCOIN", implementation = SupportedBlockchain.class)
|
||||
public SupportedBlockchain foreignBlockchain;
|
||||
|
||||
@Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long foreignAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
public int tradeTimeout;
|
||||
|
||||
@Schema(description = "Foreign blockchain address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotCreateRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequest {
|
||||
|
||||
@Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
public String atAddress;
|
||||
|
||||
@Deprecated
|
||||
@Schema(description = "Bitcoin BIP32 extended private key. DEPRECATED: use foreignKey instead", hidden = true,
|
||||
example = "xprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Foreign blockchain private key, e.g. BIP32 'm' key for Bitcoin/Litecoin starting with 'xprv'",
|
||||
example = "xprv___________________________________________________________________________________________________________")
|
||||
public String foreignKey;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequest() {
|
||||
}
|
||||
|
||||
}
|
@@ -7,18 +7,16 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
@@ -28,26 +26,38 @@ import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountLevel;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.PublicizeTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
|
||||
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@Path("/addresses")
|
||||
@Tag(name = "Addresses")
|
||||
public class AddressesResource {
|
||||
@@ -66,32 +76,18 @@ public class AddressesResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public AccountData getAccountInfo(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
accountData = new AccountData(address);
|
||||
else {
|
||||
// Unconfirmed transactions could update lastReference
|
||||
Account account = new Account(repository, address);
|
||||
|
||||
// Use last reference based on unconfirmed transactions if possible
|
||||
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
|
||||
|
||||
if (unconfirmedLastReference != null)
|
||||
// There are unconfirmed transactions so modify returned data
|
||||
accountData.setReference(unconfirmedLastReference);
|
||||
}
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return accountData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -100,42 +96,45 @@ public class AddressesResource {
|
||||
@GET
|
||||
@Path("/lastreference/{address}")
|
||||
@Operation(
|
||||
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
|
||||
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
|
||||
summary = "Fetch reference for next transaction to be created by address",
|
||||
description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no last-reference.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the base58-encoded transaction signature",
|
||||
description = "the base58-encoded last-reference",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String getLastReference(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
byte[] lastReference = null;
|
||||
AccountData accountData;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
|
||||
// Use last reference based on unconfirmed transactions if possible
|
||||
lastReference = account.getUnconfirmedLastReference();
|
||||
|
||||
if (lastReference == null)
|
||||
// No unconfirmed transactions so fallback to using one save in account data
|
||||
lastReference = account.getLastReference();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes request data from peers instead of the local db
|
||||
accountData = LiteNode.getInstance().fetchAccountData(address);
|
||||
}
|
||||
else {
|
||||
// All other node types request data from local db
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
accountData = repository.getAccountRepository().getAccount(address);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
if(lastReference == null || lastReference.length == 0) {
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] lastReference = accountData.getReference();
|
||||
|
||||
if (lastReference == null || lastReference.length == 0)
|
||||
return "false";
|
||||
} else {
|
||||
return Base58.encode(lastReference);
|
||||
}
|
||||
|
||||
return Base58.encode(lastReference);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -166,7 +165,7 @@ public class AddressesResource {
|
||||
)
|
||||
@ApiErrors({ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ApiOnlineAccount> getOnlineAccounts() {
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
|
||||
// Map OnlineAccountData entries to OnlineAccount via reward-share data
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -188,11 +187,71 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/online/levels")
|
||||
@Operation(
|
||||
summary = "Return currently 'online' accounts counts, grouped by level",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "online accounts",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = ApiOnlineAccount.class)))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<OnlineAccountLevel> getOnlineAccountsByLevel() {
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<OnlineAccountLevel> onlineAccountLevels = new ArrayList<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
try {
|
||||
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());
|
||||
|
||||
OnlineAccountLevel onlineAccountLevel = onlineAccountLevels.stream()
|
||||
.filter(a -> a.getLevel() == minterLevel)
|
||||
.findFirst().orElse(null);
|
||||
|
||||
// Note: I don't think we can use the level as the List index here because there will be gaps.
|
||||
// So we are forced to manually look up the existing item each time.
|
||||
// There's probably a nice shorthand java way of doing this, but this approach gets the same result.
|
||||
|
||||
if (onlineAccountLevel == null) {
|
||||
// No entry exists for this level yet, so create one
|
||||
onlineAccountLevel = new OnlineAccountLevel(minterLevel, 1);
|
||||
onlineAccountLevels.add(onlineAccountLevel);
|
||||
}
|
||||
else {
|
||||
// Already exists - so increment the count
|
||||
int existingCount = onlineAccountLevel.getCount();
|
||||
onlineAccountLevel.setCount(++existingCount);
|
||||
|
||||
// Then replace the existing item
|
||||
int index = onlineAccountLevels.indexOf(onlineAccountLevel);
|
||||
onlineAccountLevels.set(index, onlineAccountLevel);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by level
|
||||
onlineAccountLevels.sort(Comparator.comparingInt(OnlineAccountLevel::getLevel));
|
||||
|
||||
return onlineAccountLevels;
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance/{address}")
|
||||
@Operation(
|
||||
summary = "Returns account balance",
|
||||
description = "Returns account's balance, optionally of given asset and at given height",
|
||||
description = "Returns account's QORT balance, or of other specified asset",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the balance",
|
||||
@@ -202,8 +261,7 @@ public class AddressesResource {
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.INVALID_ASSET_ID, ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||
public BigDecimal getBalance(@PathParam("address") String address,
|
||||
@QueryParam("assetId") Long assetId,
|
||||
@QueryParam("height") Integer height) {
|
||||
@QueryParam("assetId") Long assetId) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
@@ -215,12 +273,7 @@ public class AddressesResource {
|
||||
else if (!repository.getAssetRepository().assetExists(assetId))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
|
||||
|
||||
if (height == null)
|
||||
height = repository.getBlockRepository().getBlockchainHeight();
|
||||
else if (height <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
|
||||
return account.getBalance(assetId, height);
|
||||
return Amounts.toBigDecimal(account.getConfirmedBalance(assetId));
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@@ -414,4 +467,120 @@ public class AddressesResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/publicize")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, PUBLICIZE transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = PublicizeTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String publicize(PublicizeTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/publicize/compute")
|
||||
@Operation(
|
||||
summary = "Compute nonce for raw, unsigned PUBLICIZE transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "raw, unsigned PUBLICIZE transaction in base58 encoding",
|
||||
example = "raw transaction base58"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String computePublicize(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String rawBytes58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] rawBytes = Base58.decode(rawBytes58);
|
||||
// We're expecting unsigned transaction, so append empty signature prior to decoding
|
||||
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||
|
||||
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
if (transactionData.getType() != TransactionType.PUBLICIZE)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
PublicizeTransaction publicizeTransaction = (PublicizeTransaction) Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Quicker validity check first before we compute nonce
|
||||
ValidationResult result = publicizeTransaction.isValid();
|
||||
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
publicizeTransaction.computeNonce();
|
||||
|
||||
// Re-check, but ignores signature
|
||||
result = publicizeTransaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
// Strip zeroed signature
|
||||
transactionData.setSignature(null);
|
||||
|
||||
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -21,32 +22,28 @@ import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.ActivitySummary;
|
||||
import org.qortal.api.model.NodeInfo;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -56,6 +53,7 @@ import org.qortal.network.PeerAddress;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@@ -65,6 +63,8 @@ import com.google.common.collect.Lists;
|
||||
@Tag(name = "Admin")
|
||||
public class AdminResource {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AdminResource.class);
|
||||
|
||||
private static final int MAX_LOG_LINES = 500;
|
||||
|
||||
@Context
|
||||
@@ -74,7 +74,8 @@ public class AdminResource {
|
||||
@Path("/unused")
|
||||
@Parameter(in = ParameterIn.PATH, name = "assetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer"))
|
||||
@Parameter(in = ParameterIn.PATH, name = "otherassetid", description = "Asset ID, 0 is native coin", schema = @Schema(type = "integer"))
|
||||
@Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
|
||||
@Parameter(in = ParameterIn.PATH, name = "address", description = "An account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
|
||||
@Parameter(in = ParameterIn.PATH, name = "path", description = "Local path to folder containing the files", schema = @Schema(type = "String", defaultValue = "/Users/user/Documents/MyStaticWebsite"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", schema = @Schema(type = "integer"))
|
||||
@@ -116,10 +117,41 @@ public class AdminResource {
|
||||
nodeInfo.uptime = System.currentTimeMillis() - Controller.startTime;
|
||||
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
|
||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||
nodeInfo.type = getNodeType();
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
private String getNodeType() {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
return "lite";
|
||||
}
|
||||
else if (Settings.getInstance().isTopOnly()) {
|
||||
return "topOnly";
|
||||
}
|
||||
else {
|
||||
return "full";
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
@Operation(
|
||||
summary = "Fetch node status",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = NodeStatus.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
public NodeStatus status() {
|
||||
NodeStatus nodeStatus = new NodeStatus();
|
||||
|
||||
return nodeStatus;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stop")
|
||||
@Operation(
|
||||
@@ -132,7 +164,8 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public String shutdown() {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String shutdown(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
new Thread(() -> {
|
||||
@@ -160,7 +193,10 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public ActivitySummary summary() {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ActivitySummary summary(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
ActivitySummary summary = new ActivitySummary();
|
||||
|
||||
LocalDate date = LocalDate.now();
|
||||
@@ -172,16 +208,13 @@ public class AdminResource {
|
||||
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
|
||||
int endHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
summary.blockCount = endHeight - startHeight;
|
||||
summary.setBlockCount(endHeight - startHeight);
|
||||
|
||||
summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
|
||||
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
|
||||
|
||||
for (Integer count : summary.transactionCountByType.values())
|
||||
summary.transactionCount += count;
|
||||
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
|
||||
|
||||
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
|
||||
|
||||
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
|
||||
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
|
||||
|
||||
return summary;
|
||||
} catch (DataException e) {
|
||||
@@ -189,6 +222,30 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/enginestats")
|
||||
@Operation(
|
||||
summary = "Fetch statistics snapshot for core engine",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = Controller.StatsSnapshot.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public Controller.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return Controller.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
@@ -202,6 +259,7 @@ public class AdminResource {
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<MintingAccountData> getMintingAccounts() {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<MintingAccountData> mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||
|
||||
@@ -216,7 +274,7 @@ public class AdminResource {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return new MintingAccountData(mintingAccountData.getPrivateKey(), rewardShareData);
|
||||
return new MintingAccountData(mintingAccountData, rewardShareData);
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return mintingAccounts;
|
||||
@@ -245,7 +303,10 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
|
||||
public String addMintingAccount(String seed58) {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String addMintingAccount(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String seed58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
|
||||
@@ -258,14 +319,15 @@ public class AdminResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
// Qortal: check reward-share's minting account is still allowed to mint
|
||||
PublicKeyAccount rewardShareMintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
|
||||
Account rewardShareMintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!rewardShareMintingAccount.canMint())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.CANNOT_MINT);
|
||||
|
||||
MintingAccountData mintingAccountData = new MintingAccountData(seed);
|
||||
MintingAccountData mintingAccountData = new MintingAccountData(mintingAccount.getPrivateKey(), mintingAccount.getPublicKey());
|
||||
|
||||
repository.getAccountRepository().save(mintingAccountData);
|
||||
repository.saveChanges();
|
||||
repository.exportNodeLocalData();//after adding new minting account let's persist it to the backup MintingAccounts.json
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e);
|
||||
} catch (DataException e) {
|
||||
@@ -278,13 +340,13 @@ public class AdminResource {
|
||||
@DELETE
|
||||
@Path("/mintingaccounts")
|
||||
@Operation(
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using private key",
|
||||
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "private key"
|
||||
type = "string", example = "public or private key"
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -295,14 +357,18 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
|
||||
public String deleteMintingAccount(String seed58) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] seed = Base58.decode(seed58.trim());
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String deleteMintingAccount(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (repository.getAccountRepository().delete(seed) == 0)
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
byte[] key = Base58.decode(key58.trim());
|
||||
|
||||
if (repository.getAccountRepository().delete(key) == 0)
|
||||
return "false";
|
||||
|
||||
repository.saveChanges();
|
||||
repository.exportNodeLocalData();//after removing new minting account let's persist it to the backup MintingAccounts.json
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e);
|
||||
} catch (DataException e) {
|
||||
@@ -328,6 +394,10 @@ public class AdminResource {
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
name = "tail",
|
||||
description = "Fetch most recent log lines",
|
||||
schema = @Schema(type = "boolean")
|
||||
) @QueryParam("tail") Boolean tail, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
|
||||
@@ -343,6 +413,13 @@ public class AdminResource {
|
||||
if (reverse != null && reverse)
|
||||
logLines = Lists.reverse(logLines);
|
||||
|
||||
// Tail mode - return the last X lines (where X = limit)
|
||||
if (tail != null && tail) {
|
||||
if (limit != null && limit > 0) {
|
||||
offset = logLines.size() - limit;
|
||||
}
|
||||
}
|
||||
|
||||
// offset out of bounds?
|
||||
if (offset != null && (offset < 0 || offset >= logLines.size()))
|
||||
return "";
|
||||
@@ -363,7 +440,7 @@ public class AdminResource {
|
||||
|
||||
limit = Math.min(limit, logLines.size());
|
||||
|
||||
logLines.subList(limit - 1, logLines.size()).clear();
|
||||
logLines.subList(limit, logLines.size()).clear();
|
||||
|
||||
return String.join("\n", logLines);
|
||||
} catch (IOException e) {
|
||||
@@ -392,7 +469,8 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
|
||||
public String orphan(String targetHeightString) {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String orphan(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String targetHeightString) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
@@ -401,6 +479,23 @@ public class AdminResource {
|
||||
if (targetHeight <= 0 || targetHeight > Controller.getInstance().getChainHeight())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
|
||||
// Make sure we're not orphaning as far back as the archived blocks
|
||||
// FUTURE: we could support this by first importing earlier blocks from the archive
|
||||
if (Settings.getInstance().isTopOnly() ||
|
||||
Settings.getInstance().isArchiveEnabled()) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find the first unarchived block
|
||||
int oldestBlock = repository.getBlockArchiveRepository().getBlockArchiveHeight();
|
||||
// Add some extra blocks just in case we're currently archiving/pruning
|
||||
oldestBlock += 100;
|
||||
if (targetHeight <= oldestBlock) {
|
||||
LOGGER.info("Unable to orphan beyond block {} because it is archived", oldestBlock);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BlockChain.orphan(targetHeight))
|
||||
return "true";
|
||||
else
|
||||
@@ -409,8 +504,6 @@ public class AdminResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +528,8 @@ public class AdminResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
public String forceSync(String targetPeerAddress) {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String forceSync(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String targetPeerAddress) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try {
|
||||
@@ -443,7 +537,7 @@ public class AdminResource {
|
||||
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
|
||||
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
|
||||
|
||||
if (targetPeer == null)
|
||||
@@ -457,7 +551,7 @@ public class AdminResource {
|
||||
SynchronizationResult syncResult;
|
||||
try {
|
||||
do {
|
||||
syncResult = Controller.getInstance().actuallySynchronize(targetPeer, true);
|
||||
syncResult = Synchronizer.getInstance().actuallySynchronize(targetPeer, true);
|
||||
} while (syncResult == SynchronizationResult.OK);
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
@@ -466,8 +560,6 @@ public class AdminResource {
|
||||
return syncResult.name();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (UnknownHostException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -475,4 +567,262 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Export sensitive/node-local data from repository.",
|
||||
description = "Exports data to .json files on local machine"
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String exportRepository(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.exportNodeLocalData();
|
||||
return "true";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Import data into repository.",
|
||||
description = "Imports data from file on local machine. Filename is forced to 'qortal-backup/TradeBotStates.json' if apiKey is not set.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "qortal-backup/TradeBotStates.json"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String importRepository(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String filename) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.importDataFromFile(filename);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform import
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/checkpoint")
|
||||
@Operation(
|
||||
summary = "Checkpoint data in repository.",
|
||||
description = "Forces repository to checkpoint uncommitted writes.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String checkpointRepository(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||
|
||||
return "true";
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/backup")
|
||||
@Operation(
|
||||
summary = "Perform online backup of repository.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String backupRepository(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
// Timeout if the database isn't ready for backing up after 60 seconds
|
||||
long timeout = 60 * 1000L;
|
||||
repository.backup(true, "backup", timeout);
|
||||
repository.saveChanges();
|
||||
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
// We couldn't lock blockchain to perform backup
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
||||
summary = "Perform maintenance on repository.",
|
||||
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public void performRepositoryMaintenance(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
// Timeout if the database isn't ready to start after 60 seconds
|
||||
long timeout = 60 * 1000L;
|
||||
repository.performPeriodicMaintenance(timeout);
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// No big deal
|
||||
} catch (DataException | TimeoutException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/importarchivedtrades")
|
||||
@Operation(
|
||||
summary = "Imports archived trades from TradeBotStatesArchive.json",
|
||||
description = "This can be used to recover trades that exist in the archive only, which may be needed if a<br />" +
|
||||
"problem occurred during the proof-of-work computation stage of a buy request.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public boolean importArchivedTrades(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.importDataFromFile("qortal-backup/TradeBotStatesArchive.json");
|
||||
repository.saveChanges();
|
||||
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform import
|
||||
return false;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/apikey/generate")
|
||||
@Operation(
|
||||
summary = "Generate an API key",
|
||||
description = "This request is unauthenticated if no API key has been generated yet. " +
|
||||
"If an API key already exists, it needs to be passed as a header and this endpoint " +
|
||||
"will then generate a new key which replaces the existing one.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "API key string",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String generateApiKey(@HeaderParam(Security.API_KEY_HEADER) String apiKeyHeader) {
|
||||
ApiKey apiKey = Security.getApiKey(request);
|
||||
|
||||
// If the API key is already generated, we need to authenticate this request
|
||||
if (apiKey.generated() && apiKey.exists()) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
}
|
||||
|
||||
// Not generated yet - so we are safe to generate one
|
||||
// FUTURE: we may want to restrict this to local/loopback only?
|
||||
|
||||
try {
|
||||
apiKey.generate();
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Unable to generate API key");
|
||||
}
|
||||
|
||||
return apiKey.toString();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/apikey/test")
|
||||
@Operation(
|
||||
summary = "Test an API key",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if authenticated",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String testApiKey(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return "true";
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,11 +1,17 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.extensions.Extension;
|
||||
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.qortal.api.Security;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
|
||||
tags = {
|
||||
@@ -13,7 +19,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@Tag(name = "Admin"),
|
||||
@Tag(name = "Arbitrary"),
|
||||
@Tag(name = "Assets"),
|
||||
@Tag(name = "Automated Transactions"),
|
||||
@Tag(name = "Blocks"),
|
||||
@Tag(name = "Chat"),
|
||||
@Tag(name = "Cross-Chain"),
|
||||
@Tag(name = "Groups"),
|
||||
@Tag(name = "Names"),
|
||||
@Tag(name = "Payments"),
|
||||
@@ -27,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
})
|
||||
}
|
||||
)
|
||||
@SecuritySchemes({
|
||||
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
|
||||
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
|
||||
})
|
||||
public class ApiDefinition {
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -410,7 +410,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public OrderData getAssetOrder(@PathParam("orderid") String orderId58) {
|
||||
// Decode orderID
|
||||
@@ -424,7 +424,7 @@ public class AssetsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
|
||||
if (orderData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
|
||||
|
||||
return orderData;
|
||||
} catch (DataException e) {
|
||||
@@ -451,7 +451,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ORDER_ID, ApiError.ORDER_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TradeData> getAssetOrderTrades(@PathParam("orderid") String orderId58, @Parameter(
|
||||
ref = "limit"
|
||||
@@ -471,7 +471,7 @@ public class AssetsResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
|
||||
if (orderData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_UNKNOWN);
|
||||
|
||||
return repository.getAssetRepository().getOrdersTrades(orderId, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
@@ -497,7 +497,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<OrderData> getAccountOrders(@PathParam("address") String address, @QueryParam("includeClosed") boolean includeClosed,
|
||||
@QueryParam("includeFulfilled") boolean includeFulfilled, @Parameter(
|
||||
@@ -514,11 +514,11 @@ public class AssetsResource {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
return repository.getAssetRepository().getAccountsOrders(publicKey, includeClosed, includeFulfilled, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
@@ -546,7 +546,7 @@ public class AssetsResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<OrderData> getAccountAssetPairOrders(@PathParam("address") String address, @PathParam("assetid") int assetId,
|
||||
@PathParam("otherassetid") int otherAssetId, @QueryParam("isClosed") Boolean isClosed, @QueryParam("isFulfilled") Boolean isFulfilled, @Parameter(
|
||||
@@ -563,11 +563,11 @@ public class AssetsResource {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_NO_EXISTS);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
if (!repository.getAssetRepository().assetExists(assetId))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
|
||||
|
204
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
204
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@Path("/at")
|
||||
@Tag(name = "Automated Transactions")
|
||||
public class AtResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Path("/byfunction/{codehash}")
|
||||
@Operation(
|
||||
summary = "Find automated transactions with matching functionality (code hash)",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transactions",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = ATData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<ATData> getByFunctionality(
|
||||
@PathParam("codehash")
|
||||
String codeHash58,
|
||||
@Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)")
|
||||
@QueryParam("isExecutable")
|
||||
Boolean isExecutable,
|
||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||
// Decode codeHash
|
||||
byte[] codeHash;
|
||||
try {
|
||||
codeHash = Base58.decode(codeHash58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
}
|
||||
|
||||
// codeHash must be present and have correct length
|
||||
if (codeHash == null || codeHash.length != 32)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Impose a limit on 'limit'
|
||||
if (limit != null && limit > 100)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ataddress}")
|
||||
@Operation(
|
||||
summary = "Fetch info associated with AT address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transaction",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = ATData.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public ATData getByAddress(@PathParam("ataddress") String atAddress) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().fromATAddress(atAddress);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ataddress}/data")
|
||||
@Operation(
|
||||
summary = "Fetch data segment associated with AT address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "automated transaction",
|
||||
content = @Content(
|
||||
schema = @Schema(implementation = byte[].class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
return dataBytes;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, DEPLOY_AT transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = DeployAtTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String createDeployAt(DeployAtTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@@ -8,7 +9,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -20,19 +28,25 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockMinterSummary;
|
||||
import org.qortal.api.model.BlockMintingInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@Path("/blocks")
|
||||
@@ -59,9 +73,10 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getBlock(@PathParam("signature") String signature58) {
|
||||
public BlockData getBlock(@PathParam("signature") String signature58,
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
// Decode signature
|
||||
byte[] signature;
|
||||
try {
|
||||
@@ -71,14 +86,101 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().fromSignature(signature);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
// Not found, so try the block archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/signature/{signature}/data")
|
||||
@Operation(
|
||||
summary = "Fetch serialized, base58 encoded block data using base58 signature",
|
||||
description = "Returns serialized data for the block that matches the given signature, and an optional block serialization version",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the block data",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String getSerializedBlockData(@PathParam("signature") String signature58, @QueryParam("version") Integer version) {
|
||||
// Decode signature
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Base58.decode(signature58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Default to version 1
|
||||
if (version == null) {
|
||||
version = 1;
|
||||
}
|
||||
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
Block block = new Block(repository, blockData);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
switch (version) {
|
||||
case 1:
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
bytes.write(BlockTransformer.toBytesV2(block));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
|
||||
return Base58.encode(bytes.toByteArray());
|
||||
}
|
||||
|
||||
// Not found, so try the block archive
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (bytes != null) {
|
||||
if (version != 1) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
|
||||
}
|
||||
return Base58.encode(bytes);
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
||||
} catch (DataException | IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/signature/{signature}/transactions")
|
||||
@Operation(
|
||||
@@ -98,7 +200,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
|
||||
ref = "limit"
|
||||
@@ -116,12 +218,14 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
// Check if the block exists in either the database or archive
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0 &&
|
||||
repository.getBlockArchiveRepository().getHeightFromSignature(signature) == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -144,13 +248,23 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getFirstBlock() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().fromHeight(1);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(1);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
// Try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(1);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -173,13 +287,17 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getLastBlock() {
|
||||
public BlockData getLastBlock(@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getLastBlock();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
BlockData blockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
return blockData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -202,7 +320,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getChild(@PathParam("signature") String signature58) {
|
||||
// Decode signature
|
||||
@@ -214,21 +332,30 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData childBlockData = null;
|
||||
|
||||
// Check if block exists in database
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return repository.getBlockRepository().fromReference(signature);
|
||||
}
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
|
||||
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
|
||||
// Not found, so try the archive
|
||||
// This also checks that the parent block exists
|
||||
// It will return null if either the parent or child don't exit
|
||||
childBlockData = repository.getBlockArchiveRepository().fromReference(signature);
|
||||
|
||||
// Check child block exists
|
||||
if (childBlockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
if (childBlockData == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
// Check child block's reference matches the supplied signature
|
||||
if (!Arrays.equals(childBlockData.getReference(), signature)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
return childBlockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -257,8 +384,6 @@ public class BlocksResource {
|
||||
public int getHeight() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockchainHeight();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -282,7 +407,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public int getHeight(@PathParam("signature") String signature58) {
|
||||
// Decode signature
|
||||
@@ -294,15 +419,20 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return blockData.getHeight();
|
||||
}
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return blockData.getHeight();
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData.getHeight();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -325,17 +455,103 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getByHeight(@PathParam("height") int height) {
|
||||
public BlockData getByHeight(@PathParam("height") int height,
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/byheight/{height}/mintinginfo")
|
||||
@Operation(
|
||||
summary = "Fetch block minter info using block height",
|
||||
description = "Returns the minter info for the block with given height",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the block",
|
||||
content = @Content(
|
||||
schema = @Schema(
|
||||
implementation = BlockData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockMintingInfo getBlockMintingInfoByHeight(@PathParam("height") int height) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Try the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
|
||||
// Still not found
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
BlockData parentBlockData = repository.getBlockRepository().fromSignature(blockData.getReference());
|
||||
if (parentBlockData == null) {
|
||||
// Parent block not found - try the archive
|
||||
parentBlockData = repository.getBlockArchiveRepository().fromSignature(blockData.getReference());
|
||||
if (parentBlockData == null) {
|
||||
|
||||
// Still not found
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||
if (minterLevel == 0)
|
||||
// This may be unavailable when requesting a trimmed block
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
BigInteger distance = block.calcKeyDistance(parentBlockData.getHeight(), parentBlockData.getSignature(), blockData.getMinterPublicKey(), minterLevel);
|
||||
double ratio = new BigDecimal(distance).divide(new BigDecimal(block.MAX_DISTANCE), 40, RoundingMode.DOWN).doubleValue();
|
||||
long timestamp = block.calcTimestamp(parentBlockData, blockData.getMinterPublicKey(), minterLevel);
|
||||
long timeDelta = timestamp - parentBlockData.getTimestamp();
|
||||
|
||||
BlockMintingInfo blockMintingInfo = new BlockMintingInfo();
|
||||
blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey();
|
||||
blockMintingInfo.minterLevel = minterLevel;
|
||||
blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||
blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE);
|
||||
blockMintingInfo.keyDistance = distance;
|
||||
blockMintingInfo.keyDistanceRatio = ratio;
|
||||
blockMintingInfo.timestamp = timestamp;
|
||||
blockMintingInfo.timeDelta = timeDelta;
|
||||
|
||||
return blockMintingInfo;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -357,21 +573,41 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.BLOCK_UNKNOWN, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp) {
|
||||
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp,
|
||||
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
BlockData blockData = null;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
|
||||
// Try the Blocks table
|
||||
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height > 1) {
|
||||
// Found match in Blocks table
|
||||
blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
// Not found in Blocks table, so try the archive
|
||||
height = repository.getBlockArchiveRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height > 1) {
|
||||
// Found match in archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
}
|
||||
|
||||
// Ensure block exists
|
||||
if (blockData == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||
blockData.setOnlineAccountsSignatures(null);
|
||||
}
|
||||
|
||||
return blockData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -396,7 +632,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
|
||||
ref = "count"
|
||||
@@ -406,25 +642,28 @@ public class BlocksResource {
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++height) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
// Run out of blocks!
|
||||
break;
|
||||
if (blockData == null) {
|
||||
// Not found - try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
// Run out of blocks!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
blocks.add(blockData);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/minter/{address}")
|
||||
@Path("/signer/{address}")
|
||||
@Operation(
|
||||
summary = "Fetch block summaries for blocks minted by address",
|
||||
summary = "Fetch block summaries for blocks signed by address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "block summaries",
|
||||
@@ -439,7 +678,7 @@ public class BlocksResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<BlockSummaryData> getBlockSummariesByMinter(@PathParam("address") String address, @Parameter(
|
||||
public List<BlockSummaryData> getBlockSummariesBySigner(@PathParam("address") String address, @Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
@@ -455,32 +694,52 @@ public class BlocksResource {
|
||||
if (accountData == null || accountData.getPublicKey() == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
|
||||
|
||||
return repository.getBlockRepository().getBlockSummariesByMinter(accountData.getPublicKey(), limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
|
||||
List<BlockSummaryData> summaries = repository.getBlockRepository()
|
||||
.getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
|
||||
// Add any from the archive
|
||||
List<BlockSummaryData> archivedSummaries = repository.getBlockArchiveRepository()
|
||||
.getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
if (archivedSummaries != null && !archivedSummaries.isEmpty()) {
|
||||
summaries.addAll(archivedSummaries);
|
||||
}
|
||||
else {
|
||||
summaries = archivedSummaries;
|
||||
}
|
||||
|
||||
// Sort the results (because they may have been obtained from two places)
|
||||
if (reverse != null && reverse) {
|
||||
summaries.sort((s1, s2) -> Integer.valueOf(s2.getHeight()).compareTo(Integer.valueOf(s1.getHeight())));
|
||||
}
|
||||
else {
|
||||
summaries.sort(Comparator.comparing(s -> Integer.valueOf(s.getHeight())));
|
||||
}
|
||||
|
||||
return summaries;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/minters")
|
||||
@Path("/signers")
|
||||
@Operation(
|
||||
summary = "Show summary of block minters",
|
||||
description = "Returns count of blocks minted, optionally limited to minters/recipients in passed address(es).",
|
||||
summary = "Show summary of block signers",
|
||||
description = "Returns count of blocks signed, optionally limited to minters/recipients in passed address(es).",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockMinterSummary.class
|
||||
implementation = BlockSignerSummary.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<BlockMinterSummary> getBlockMinters(@QueryParam("address") List<String> addresses,
|
||||
public List<BlockSignerSummary> getBlockSigners(@QueryParam("address") List<String> addresses,
|
||||
@Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
@@ -493,7 +752,117 @@ public class BlocksResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
return repository.getBlockRepository().getBlockMinters(addresses, limit, offset, reverse);
|
||||
// This method pulls data from both Blocks and BlockArchive, so no need to query serparately
|
||||
return repository.getBlockArchiveRepository().getBlockSigners(addresses, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/summaries")
|
||||
@Operation(
|
||||
summary = "Fetch only summary info about a range of blocks",
|
||||
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "blocks",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = BlockSummaryData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<BlockSummaryData> getBlockSummaries(
|
||||
@QueryParam("start") Integer startHeight,
|
||||
@QueryParam("end") Integer endHeight,
|
||||
@Parameter(ref = "count") @QueryParam("count") Integer count) {
|
||||
// Check up to 2 out of 3 params
|
||||
if (startHeight != null && endHeight != null && count != null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check values
|
||||
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
|
||||
*
|
||||
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
|
||||
* 30 null null blocks 30 to 79 (assume count=50)
|
||||
* 30 null 10 blocks 30 to 39
|
||||
*
|
||||
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
|
||||
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
|
||||
* null 200 10 blocks 190 to 199 (excludes end block)
|
||||
*/
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
// Use the latest X blocks if only a count is specified
|
||||
if (startHeight == null && endHeight == null && count != null) {
|
||||
BlockData chainTip = repository.getBlockRepository().getLastBlock();
|
||||
startHeight = chainTip.getHeight() - count;
|
||||
endHeight = chainTip.getHeight();
|
||||
}
|
||||
|
||||
// ... otherwise default the start height to 1
|
||||
if (startHeight == null && endHeight == null) {
|
||||
startHeight = 1;
|
||||
}
|
||||
|
||||
// Default the count to 50
|
||||
if (count == null) {
|
||||
count = 50;
|
||||
}
|
||||
|
||||
// If both a start and end height exist, ignore the count
|
||||
if (startHeight != null && endHeight != null) {
|
||||
if (startHeight > 0 && endHeight > 0) {
|
||||
count = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
// Derive start height from end height if missing
|
||||
if (startHeight == null || startHeight == 0) {
|
||||
if (endHeight != null && endHeight > 0) {
|
||||
if (count != null) {
|
||||
startHeight = endHeight - count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++startHeight) {
|
||||
if (endHeight != null && startHeight >= endHeight) {
|
||||
break;
|
||||
}
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(startHeight);
|
||||
if (blockData == null) {
|
||||
// Not found - try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(startHeight);
|
||||
if (blockData == null) {
|
||||
// Run out of blocks!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockData != null) {
|
||||
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
|
||||
blockSummaries.add(blockSummaryData);
|
||||
}
|
||||
}
|
||||
|
||||
return blockSummaries;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user