mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-04-19 15:26:03 -04:00
Compare commits
650 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
13e13d945d | ||
![]() |
bfef0f9492 | ||
![]() |
39909e7936 | ||
![]() |
695f2b8dd3 | ||
![]() |
8f778be4d9 | ||
![]() |
821ef16d8f | ||
![]() |
58590d60df | ||
![]() |
8eccbcce43 | ||
![]() |
bbfc5d5fed | ||
![]() |
22db354cb2 | ||
![]() |
60ceff6695 | ||
![]() |
c87b8a5b45 | ||
![]() |
bf055556f1 | ||
![]() |
13dc34a805 | ||
![]() |
53b0f203de | ||
![]() |
52f0c20c8c | ||
![]() |
a30b41de4b | ||
![]() |
f1c09161f4 | ||
![]() |
57c2408d07 | ||
![]() |
f5515caad5 | ||
![]() |
71b23e0ed9 | ||
![]() |
9a5d2d5862 | ||
![]() |
31782e5255 | ||
![]() |
96eab3d42f | ||
![]() |
454fc91298 | ||
![]() |
5bff265cca | ||
![]() |
295c91760c | ||
![]() |
f19ed19325 | ||
![]() |
31fbf9c4e8 | ||
![]() |
765a32fd9f | ||
![]() |
fe1fb88ce0 | ||
![]() |
35eb65d173 | ||
![]() |
974c6a0d86 | ||
![]() |
ad38e3b80c | ||
![]() |
7243d7fa38 | ||
![]() |
d78709e1f9 | ||
![]() |
34b55bc86b | ||
![]() |
501485ec71 | ||
![]() |
1c92d96651 | ||
![]() |
08b0b36436 | ||
![]() |
9027ce6634 | ||
![]() |
3c6914ac7e | ||
![]() |
055b7d1376 | ||
![]() |
f87dc3a4d1 | ||
![]() |
40e18890d6 | ||
![]() |
7e3a47de4a | ||
![]() |
9668dd2369 | ||
![]() |
9bd4f70d02 | ||
![]() |
39c75cd71b | ||
![]() |
8981740b8c | ||
![]() |
584cc3b6d4 | ||
![]() |
dc43e1c329 | ||
![]() |
93369c4211 | ||
![]() |
a21971429c | ||
![]() |
df9daf99bf | ||
![]() |
e699b427e2 | ||
![]() |
29e6540234 | ||
![]() |
207ff5416c | ||
![]() |
ad809ff20e | ||
![]() |
b3174518d9 | ||
![]() |
82728aef69 | ||
![]() |
1a2dcfc704 | ||
![]() |
ce27818f43 | ||
![]() |
a7d8e4560f | ||
![]() |
028ced7021 | ||
![]() |
c95a26e043 | ||
![]() |
b5f9bc307b | ||
![]() |
bee86daff3 | ||
![]() |
26e3a153bc | ||
![]() |
2af9019db0 | ||
![]() |
5107c6ba57 | ||
![]() |
79aa214f22 | ||
![]() |
6f3ae49b68 | ||
![]() |
5711aabad8 | ||
![]() |
d7be2885bd | ||
![]() |
51fc4d0c41 | ||
![]() |
1b31dc24b8 | ||
![]() |
b19724e33d | ||
![]() |
07fa0b35e4 | ||
![]() |
f4f53630d5 | ||
![]() |
cb25a23779 | ||
![]() |
b7c9dea518 | ||
![]() |
34458cf3df | ||
![]() |
63917fe8cc | ||
![]() |
d4eb30bb97 | ||
![]() |
cb69d06468 | ||
![]() |
46734459d4 | ||
![]() |
a55daf803e | ||
![]() |
1510e6f18d | ||
![]() |
fb2b4a0c6a | ||
![]() |
38c0855728 | ||
![]() |
84d8a17ab4 | ||
![]() |
00a2a7c2b7 | ||
![]() |
251a973fd6 | ||
![]() |
bedd38748e | ||
![]() |
b0e9627c10 | ||
![]() |
bf97fbc7ea | ||
![]() |
8b1d2aa203 | ||
![]() |
2d46b2ab7c | ||
![]() |
9acd7ad584 | ||
![]() |
c853c4ffcb | ||
![]() |
e5f729d12f | ||
![]() |
03a1132c2f | ||
![]() |
61a62a1d94 | ||
![]() |
a53026be8a | ||
![]() |
6b567b94f2 | ||
![]() |
c9350e123e | ||
![]() |
68f7067125 | ||
![]() |
d67d259b2c | ||
![]() |
e24b1c2461 | ||
![]() |
67d0589e7b | ||
![]() |
060d9fa4f1 | ||
![]() |
52bf1edf79 | ||
![]() |
580e5b672c | ||
![]() |
5720ee74b0 | ||
![]() |
fff0fa0186 | ||
![]() |
31b0edca22 | ||
![]() |
48501a6572 | ||
![]() |
998b893cc3 | ||
![]() |
816d273956 | ||
![]() |
28d2bc891f | ||
![]() |
964c71ed1b | ||
![]() |
40924a6f7b | ||
![]() |
8a01a07ac2 | ||
![]() |
5d457d62c5 | ||
![]() |
b9381f7f9f | ||
![]() |
4d765fa5d9 | ||
![]() |
e4fa5f520d | ||
![]() |
c3b7289943 | ||
![]() |
024e59a982 | ||
![]() |
667f0c8fb5 | ||
![]() |
0cba254193 | ||
![]() |
f675588a2d | ||
![]() |
290a3738b7 | ||
![]() |
4a82c69507 | ||
![]() |
b72159fcf8 | ||
![]() |
cd71bcdde7 | ||
![]() |
bd3fffada4 | ||
![]() |
bffcf7c7c0 | ||
![]() |
c26974610c | ||
![]() |
5f8cf97d16 | ||
![]() |
afa95f1b15 | ||
![]() |
728cf22578 | ||
![]() |
f35c7f8544 | ||
![]() |
b48dbc2fb3 | ||
![]() |
8038dcf401 | ||
![]() |
ae8760d72c | ||
![]() |
71fab722ee | ||
![]() |
c333803917 | ||
![]() |
e6b29b88f5 | ||
![]() |
352384b41e | ||
![]() |
12def5f1b5 | ||
![]() |
88c3f04be0 | ||
![]() |
97569bad37 | ||
![]() |
1d4dbe7ce0 | ||
![]() |
a014740014 | ||
![]() |
9b50cd7ba7 | ||
![]() |
6c6c6e2dd5 | ||
![]() |
a6af1550a4 | ||
![]() |
e4714aab89 | ||
![]() |
0d2c1fe8fd | ||
![]() |
dc7a8e4201 | ||
![]() |
c3f7f194b0 | ||
![]() |
3847d1bd3a | ||
![]() |
535b71adc5 | ||
![]() |
66770cc98f | ||
![]() |
39bc54df73 | ||
![]() |
7bc341d69f | ||
![]() |
9a74856fa2 | ||
![]() |
a8fb638594 | ||
![]() |
2d4455b1a2 | ||
![]() |
bf8f4cea73 | ||
![]() |
fac901331f | ||
![]() |
130a45c99a | ||
![]() |
b571b39790 | ||
![]() |
88b6bed93e | ||
![]() |
97475d84e9 | ||
![]() |
69da858365 | ||
![]() |
e1b3cdce28 | ||
![]() |
7fba0faac1 | ||
![]() |
5e6bf9e22b | ||
![]() |
0f5f7ae46e | ||
![]() |
6301bde10e | ||
![]() |
2f322674f8 | ||
![]() |
533527e362 | ||
![]() |
b0c1dceb56 | ||
![]() |
d9f9c1e736 | ||
![]() |
1ac4c45f6d | ||
![]() |
e426f4d8f1 | ||
![]() |
944c189166 | ||
![]() |
e8d5366941 | ||
![]() |
3e0b694e13 | ||
![]() |
21ea08a68d | ||
![]() |
a1a7f9ccc9 | ||
![]() |
e4f3d13660 | ||
![]() |
25f85f9f8d | ||
![]() |
a9325356c4 | ||
![]() |
9e95de2d7e | ||
![]() |
0462ddc273 | ||
![]() |
c1b17cf612 | ||
![]() |
89007c496e | ||
![]() |
2dc7405f82 | ||
![]() |
6a798312fe | ||
![]() |
fc1388d2f4 | ||
![]() |
cccd9cf094 | ||
![]() |
018ac61054 | ||
![]() |
ed87b36a76 | ||
![]() |
adcf158a90 | ||
![]() |
f053a274a4 | ||
![]() |
fdee044023 | ||
![]() |
42ede83ca2 | ||
![]() |
5444d96832 | ||
![]() |
7340ca9c21 | ||
![]() |
542441d9d2 | ||
![]() |
c5ef60ce5c | ||
![]() |
389c5dddac | ||
![]() |
7240b5f222 | ||
![]() |
34e0c4b71f | ||
![]() |
aab4d0207e | ||
![]() |
1a51b171a0 | ||
![]() |
a557d90e5d | ||
![]() |
7e4e950710 | ||
![]() |
323d14feb0 | ||
![]() |
5c79380e63 | ||
![]() |
af3c7059a9 | ||
![]() |
7e29dc188d | ||
![]() |
544d69827a | ||
![]() |
c75e3aa455 | ||
![]() |
bd5accb5a5 | ||
![]() |
775fbc41c2 | ||
![]() |
ece3b0fec0 | ||
![]() |
4b7db9a1ae | ||
![]() |
140961d885 | ||
![]() |
9ec2794931 | ||
![]() |
85acb8aeb3 | ||
![]() |
19398bb73e | ||
![]() |
0275de3ff6 | ||
![]() |
b586bc57f6 | ||
![]() |
7f6d28f1fb | ||
![]() |
1aef8a6bab | ||
![]() |
71987400c7 | ||
![]() |
e05ab6f7ed | ||
![]() |
cfaf163bbc | ||
![]() |
dc8d854709 | ||
![]() |
1f385328de | ||
![]() |
98e2df3c7e | ||
![]() |
103c45d412 | ||
![]() |
c9cf5351c0 | ||
![]() |
bf452c91da | ||
![]() |
a5417994d6 | ||
![]() |
c40e0bea5a | ||
![]() |
ae80935f3a | ||
![]() |
68b4a0fafb | ||
![]() |
8fd7f17317 | ||
![]() |
24657c6c57 | ||
![]() |
ba763f7bf6 | ||
![]() |
264cb5f0ac | ||
![]() |
c9e992442c | ||
![]() |
86e67d384c | ||
![]() |
59d8a8ee44 | ||
![]() |
5221782ba0 | ||
![]() |
023e2bcd2f | ||
![]() |
ea8badd3f6 | ||
![]() |
7a392f3c1e | ||
![]() |
dd5b7996b3 | ||
![]() |
d906ffa36a | ||
![]() |
3cdfa0fa27 | ||
![]() |
9c3e405fe0 | ||
![]() |
2c3cd5208b | ||
![]() |
0450900b37 | ||
![]() |
8f5e56b9dc | ||
![]() |
0d82f8827f | ||
![]() |
577cfa249e | ||
![]() |
b348a81f13 | ||
![]() |
8a9b4ffe11 | ||
![]() |
466e1f048e | ||
![]() |
1af87b2db9 | ||
![]() |
cdb99a9cfb | ||
![]() |
6db4812f06 | ||
![]() |
5c4fa7a53f | ||
![]() |
74e094fa99 | ||
![]() |
329fa1c39a | ||
![]() |
7094dfcc09 | ||
![]() |
5d85335dc7 | ||
![]() |
6730d023d6 | ||
![]() |
b31758e884 | ||
![]() |
2a45ebe565 | ||
![]() |
0c76c48c65 | ||
![]() |
f470112cae | ||
![]() |
123a2a8487 | ||
![]() |
bc1cfe3ba0 | ||
![]() |
ed567beeb3 | ||
![]() |
22f32f43a0 | ||
![]() |
e3420de0d8 | ||
![]() |
a9c975466e | ||
![]() |
072401386e | ||
![]() |
a00930aa9e | ||
![]() |
b0fc864313 | ||
![]() |
ee889c5238 | ||
![]() |
aee0c1c0b2 | ||
![]() |
485746381c | ||
![]() |
c855d66a0c | ||
![]() |
6b91b057e5 | ||
![]() |
6675390e20 | ||
![]() |
4821a8d284 | ||
![]() |
e0cdef8844 | ||
![]() |
3520694251 | ||
![]() |
e0244a51dc | ||
![]() |
6b3e9febf3 | ||
![]() |
6ff73f1898 | ||
![]() |
e811e2b224 | ||
![]() |
a6c178c058 | ||
![]() |
cda610fdfd | ||
![]() |
6b688194f1 | ||
![]() |
5f0d95c743 | ||
![]() |
5352225bce | ||
![]() |
58787a1d31 | ||
![]() |
ddb48d1846 | ||
![]() |
264741152a | ||
![]() |
a4223a6aab | ||
![]() |
662eaee7c3 | ||
![]() |
a53e6a0e3d | ||
![]() |
ebc28805c8 | ||
![]() |
a1b50a7b42 | ||
![]() |
e25d3fb52e | ||
![]() |
dbc841979b | ||
![]() |
d61eabc3d2 | ||
![]() |
5c0f49d58f | ||
![]() |
047f1a1c1f | ||
![]() |
7fe27b26b2 | ||
![]() |
61158e9750 | ||
![]() |
ea3f099df7 | ||
![]() |
8696c71051 | ||
![]() |
b940021d99 | ||
![]() |
b2a6708ac1 | ||
![]() |
3ffda0fdd1 | ||
![]() |
3e3f3085f8 | ||
![]() |
60b91d3d23 | ||
![]() |
11c0f7613b | ||
![]() |
1329902a55 | ||
![]() |
6c640ddbef | ||
![]() |
50f3bd510a | ||
![]() |
8d55abe3b9 | ||
![]() |
c04fc7b2db | ||
![]() |
0ed640be16 | ||
![]() |
2a9bc87f65 | ||
![]() |
2f310b420d | ||
![]() |
d4f1dc5b8e | ||
![]() |
a4e43f1045 | ||
![]() |
7306972d19 | ||
![]() |
1ea9a6f750 | ||
![]() |
c08b4d0772 | ||
![]() |
874075b371 | ||
![]() |
6d1f1e43d6 | ||
![]() |
e4f0277326 | ||
![]() |
dbbfb50cd3 | ||
![]() |
a20377fb04 | ||
![]() |
a8e76fd720 | ||
![]() |
2d0f200aa1 | ||
![]() |
cae360b6c5 | ||
![]() |
30f0cf273c | ||
![]() |
af44544e15 | ||
![]() |
6759687046 | ||
![]() |
d74b0995d0 | ||
![]() |
caebd58303 | ||
![]() |
e18d281ca9 | ||
![]() |
d3ee7340f0 | ||
![]() |
d3d30c3d0b | ||
![]() |
8cb8f9f3cf | ||
![]() |
1046caf907 | ||
![]() |
5f6f513f3c | ||
![]() |
ffe88b49a6 | ||
![]() |
38b1ace4a4 | ||
![]() |
b3d3a936d5 | ||
![]() |
a7e90395d2 | ||
![]() |
dca53533f0 | ||
![]() |
74fdf32c2a | ||
![]() |
0461fe66ec | ||
![]() |
a7cda6cdeb | ||
![]() |
0644d0b74a | ||
![]() |
e10f764dc2 | ||
![]() |
7461da9722 | ||
![]() |
d300ce8ce7 | ||
![]() |
fa25843684 | ||
![]() |
0105c1436a | ||
![]() |
443c2f4cdb | ||
![]() |
3dfaa2fc52 | ||
![]() |
d9a3feba8d | ||
![]() |
ca7d596175 | ||
![]() |
1b5c03bce8 | ||
![]() |
dbb3d4f891 | ||
![]() |
d9630a13b5 | ||
![]() |
00ceeeba5f | ||
![]() |
9004c7f32a | ||
![]() |
2e57e6197d | ||
![]() |
76f8f85487 | ||
![]() |
8d8aa65d1d | ||
![]() |
0df4f63d53 | ||
![]() |
3b0080dbba | ||
![]() |
79cd9f3e82 | ||
![]() |
d4a9838cd8 | ||
![]() |
0f0b645f72 | ||
![]() |
cf282fd930 | ||
![]() |
3d44f3777c | ||
![]() |
84a2828e90 | ||
![]() |
c63cf2f0a0 | ||
![]() |
75b96e83da | ||
![]() |
180fde87cc | ||
![]() |
a8d5c63f9f | ||
![]() |
5ca9cb8dff | ||
![]() |
ddee87f85d | ||
![]() |
1e70c70579 | ||
![]() |
bdcf8a2182 | ||
![]() |
d8161cd6e2 | ||
![]() |
629e1508f2 | ||
![]() |
b03c873a06 | ||
![]() |
7c8753c17b | ||
![]() |
b61f1fabcd | ||
![]() |
13d6eaee7d | ||
![]() |
fc3407cd50 | ||
![]() |
eb8025f6e8 | ||
![]() |
5d739f912c | ||
![]() |
82d586ab78 | ||
![]() |
291622e452 | ||
![]() |
9b26682646 | ||
![]() |
09fd8710b1 | ||
![]() |
ebcadb7bed | ||
![]() |
cb132e727a | ||
![]() |
41e63805c1 | ||
![]() |
16263bb7b3 | ||
![]() |
326cfdfb80 | ||
![]() |
0b3763f900 | ||
![]() |
7308206a10 | ||
![]() |
caaf9f7b5b | ||
![]() |
8b7e95f614 | ||
![]() |
d69dcae875 | ||
![]() |
06b0c20bad | ||
![]() |
33bf54bcac | ||
![]() |
1c381de806 | ||
![]() |
78a2476bb8 | ||
![]() |
96bcd7547d | ||
![]() |
ff0ccc21e2 | ||
![]() |
9c359b5e29 | ||
![]() |
0a469db8f6 | ||
![]() |
40421eec75 | ||
![]() |
690f38e4dd | ||
![]() |
91e442d642 | ||
![]() |
23a7fb3d16 | ||
![]() |
a149d92392 | ||
![]() |
4a6fcfae84 | ||
![]() |
c1c8c6fa85 | ||
![]() |
c2b816e5f0 | ||
![]() |
f852217945 | ||
![]() |
255bd33c47 | ||
![]() |
082c8c4290 | ||
![]() |
9747b20a27 | ||
![]() |
b0c73d1b39 | ||
![]() |
db6cb237bf | ||
![]() |
8e3e91c7cc | ||
![]() |
6de61243b3 | ||
![]() |
7c2af064a3 | ||
![]() |
05b00727a5 | ||
![]() |
8bf8144709 | ||
![]() |
31ce183c83 | ||
![]() |
5d39eecd4f | ||
![]() |
86e4f7b3f2 | ||
![]() |
ec9f91e014 | ||
![]() |
8ce91aa0c1 | ||
![]() |
c01d63490f | ||
![]() |
856faafd1c | ||
![]() |
1debdde33e | ||
![]() |
c3b93b6e75 | ||
![]() |
7acba27b9d | ||
![]() |
7ebc1bfc11 | ||
![]() |
3cac6d7c69 | ||
![]() |
0a67b9a423 | ||
![]() |
1ee5a628a8 | ||
![]() |
7a9e814145 | ||
![]() |
c29cdb8b0f | ||
![]() |
8cdd65e7dd | ||
![]() |
748f698314 | ||
![]() |
8e8b0cc7c0 | ||
![]() |
ee540d075b | ||
![]() |
ac332dfd76 | ||
![]() |
40e8c8b82c | ||
![]() |
fbb9c0f490 | ||
![]() |
17c559d000 | ||
![]() |
4fcd36edc4 | ||
![]() |
cb0b6665f7 | ||
![]() |
26a5ffcb31 | ||
![]() |
4494af8bc0 | ||
![]() |
4819e5ebfa | ||
![]() |
69a1e67da2 | ||
![]() |
5108c22a29 | ||
![]() |
f454bbbf63 | ||
![]() |
1f0e326a16 | ||
![]() |
438e8d41cb | ||
![]() |
57948b36fd | ||
![]() |
0c1ac28e26 | ||
![]() |
c22c3b82dd | ||
![]() |
9b3d78bd4b | ||
![]() |
7abbe1c36f | ||
![]() |
fea804086b | ||
![]() |
26c32a8ff4 | ||
![]() |
ca125dbc48 | ||
![]() |
37af7e5338 | ||
![]() |
99f41e0feb | ||
![]() |
31a587861a | ||
![]() |
88290c9dff | ||
![]() |
e12ec197bf | ||
![]() |
f6c35ba6f3 | ||
![]() |
f252265ede | ||
![]() |
dd28c237c9 | ||
![]() |
99bb29e0b0 | ||
![]() |
b963fd501f | ||
![]() |
0919d92ce4 | ||
![]() |
cf1fabd0bb | ||
![]() |
afcf5dd365 | ||
![]() |
879ed9b10a | ||
![]() |
7c3e87ed38 | ||
![]() |
dade4a3759 | ||
![]() |
6dfa1841f8 | ||
![]() |
2f0ea48a31 | ||
![]() |
7e898ba23d | ||
![]() |
14d17023a8 | ||
![]() |
3e2206cf5e | ||
![]() |
b63227e8db | ||
![]() |
64081b684c | ||
![]() |
3b1961fb05 | ||
![]() |
796603f82b | ||
![]() |
f2f5a6fd24 | ||
![]() |
4172cc72df | ||
![]() |
52be627cca | ||
![]() |
68a4c21b17 | ||
![]() |
35f275805b | ||
![]() |
a1e554473a | ||
![]() |
48f05cca8c | ||
![]() |
36f7037dde | ||
![]() |
d55153bd36 | ||
![]() |
0c5ed84996 | ||
![]() |
b66e6b1c12 | ||
![]() |
66e9ac7d3c | ||
![]() |
9cd28a6bde | ||
![]() |
1150d929af | ||
![]() |
6df5296dcd | ||
![]() |
3fbb2f95d0 | ||
![]() |
2e605a590e | ||
![]() |
80db207a98 | ||
![]() |
7885d95a4c | ||
![]() |
7df40580a3 | ||
![]() |
b4369fbb9f | ||
![]() |
bee93bf45f | ||
![]() |
ffdbe73693 | ||
![]() |
82b8f58579 | ||
![]() |
ea0ce9b449 | ||
![]() |
36e2f8675c | ||
![]() |
cfd89b6da1 | ||
![]() |
76859f822e | ||
![]() |
58cead6035 | ||
![]() |
e30a247898 | ||
![]() |
64acf86fbe | ||
![]() |
113a94b1f9 | ||
![]() |
8462ff1019 | ||
![]() |
1aa62863f4 | ||
![]() |
7e3d89797e | ||
![]() |
7847460f11 | ||
![]() |
aee7a539b5 | ||
![]() |
1b864368e1 | ||
![]() |
a723c0d86b | ||
![]() |
281b7d0905 | ||
![]() |
816ba912a1 | ||
![]() |
6dedce1d27 | ||
![]() |
091ffd98eb | ||
![]() |
cc247bbc69 | ||
![]() |
0fed23ec22 | ||
![]() |
b81922cd9e | ||
![]() |
1bf83ecb8b | ||
![]() |
a9f7a06e1f | ||
![]() |
7aebc7bc31 | ||
![]() |
3cdd88b569 | ||
![]() |
416761af41 | ||
![]() |
235c0682b3 | ||
![]() |
3814b5b6db | ||
![]() |
f4de560764 | ||
![]() |
f1b8cd1e2e | ||
![]() |
5d7991e4f7 | ||
![]() |
203386f03d | ||
![]() |
d64ee42154 | ||
![]() |
adf7348515 | ||
![]() |
0ea056104c | ||
![]() |
7887c450c7 | ||
![]() |
a6d827c369 | ||
![]() |
4ec5339e5d | ||
![]() |
b179203dd2 | ||
![]() |
4761b71105 | ||
![]() |
399d4e0512 | ||
![]() |
6b34651101 | ||
![]() |
78ec06b851 | ||
![]() |
6fb846d783 | ||
![]() |
b034ac8c13 | ||
![]() |
78ae449e18 | ||
![]() |
f99fab8515 | ||
![]() |
ceff34672d | ||
![]() |
b50238a805 | ||
![]() |
f53a4e5fad | ||
![]() |
e96b875232 | ||
![]() |
a5883d7bcd | ||
![]() |
eefcf0191f | ||
![]() |
2341e73da2 | ||
![]() |
f8480a1a4d | ||
![]() |
e2f19c280e | ||
![]() |
467b678ea7 | ||
![]() |
a3f2cd875c | ||
![]() |
5531d4eea1 | ||
![]() |
8e24ebfc23 | ||
![]() |
bd1be1041a | ||
![]() |
7a0c8a3f3b | ||
![]() |
d1e5910502 | ||
![]() |
7de2e9de6a | ||
![]() |
94ab3c1f9b | ||
![]() |
e63141279c | ||
![]() |
f519ac12a5 | ||
![]() |
895acc9d7c | ||
![]() |
2e672260d3 | ||
![]() |
6455171dea | ||
![]() |
3a66c9cd24 | ||
![]() |
e4b80ef14b | ||
![]() |
d0a25d7d5b | ||
![]() |
adccf27385 | ||
![]() |
f0862b7aeb | ||
![]() |
9d9635ff50 | ||
![]() |
5c0d9a1ae5 | ||
![]() |
0ead6d8f83 | ||
![]() |
b08d6833a8 | ||
![]() |
8097b0f499 | ||
![]() |
a107acbdb4 | ||
![]() |
97d35dda33 | ||
![]() |
ca2d7704ab | ||
![]() |
9cbf042da2 | ||
![]() |
f7ac9ae37a | ||
![]() |
b86e916dcb | ||
![]() |
b6a113b742 | ||
![]() |
9062bc9159 | ||
![]() |
ccf2757418 | ||
![]() |
2ba37d98fe | ||
![]() |
e629a8c63a | ||
![]() |
ae08caa287 | ||
![]() |
7d348febab | ||
![]() |
4a1a4f359e | ||
![]() |
de07a926c2 |
146
.github/workflows/build.yml
vendored
146
.github/workflows/build.yml
vendored
@ -1,3 +1,6 @@
|
||||
# GitHub Releases requires a tag, e.g:
|
||||
# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1"
|
||||
# git push origin 1.0.19-1
|
||||
name: CI
|
||||
|
||||
on:
|
||||
@ -11,7 +14,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-22.04, macos-13, windows-latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -26,8 +29,145 @@ jobs:
|
||||
cache: gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --stacktrace --scan
|
||||
- name: cache nodes dependencies
|
||||
uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: error-reports-${{ matrix.os }}
|
||||
path: ${{ github.workspace }}/desktop/build/reports
|
||||
- name: cache nodes dependencies
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
include-hidden-files: true
|
||||
name: cached-localnet
|
||||
path: .localnet
|
||||
overwrite: true
|
||||
- name: Install dependencies
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
|
||||
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
- name: Install WiX Toolset
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe
|
||||
.\wix314.exe /quiet /norestart
|
||||
shell: powershell
|
||||
- name: Build Haveno Installer
|
||||
run: |
|
||||
./gradlew clean build --refresh-keys --refresh-dependencies
|
||||
./gradlew packageInstallers
|
||||
working-directory: .
|
||||
|
||||
# get version from jar
|
||||
- name: Set Version Unix
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
|
||||
run: |
|
||||
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
- name: Set Version Windows
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
|
||||
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
shell: powershell
|
||||
|
||||
- name: Move Release Files for Linux
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
run: |
|
||||
mkdir ${{ github.workspace }}/release-linux-rpm
|
||||
mkdir ${{ github.workspace }}/release-linux-deb
|
||||
mkdir ${{ github.workspace }}/release-linux-flatpak
|
||||
mkdir ${{ github.workspace }}/release-linux-appimage
|
||||
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
||||
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
||||
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
||||
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||
shell: bash
|
||||
- name: Move Release Files for macOS
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
run: |
|
||||
mkdir ${{ github.workspace }}/release-macos
|
||||
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
|
||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
|
||||
shell: bash
|
||||
- name: Move Release Files on Windows
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
mkdir ${{ github.workspace }}/release-windows
|
||||
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
|
||||
Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows
|
||||
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
||||
shell: powershell
|
||||
|
||||
# win
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Windows artifacts"
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
name: haveno-windows
|
||||
path: ${{ github.workspace }}/release-windows
|
||||
# macos
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "macOS artifacts"
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
with:
|
||||
name: haveno-macos
|
||||
path: ${{ github.workspace }}/release-macos
|
||||
# linux
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - deb artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-deb
|
||||
path: ${{ github.workspace }}/release-linux-deb
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - rpm artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-rpm
|
||||
path: ${{ github.workspace }}/release-linux-rpm
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - AppImage artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-appimage
|
||||
path: ${{ github.workspace }}/release-linux-appimage
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - flatpak artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-flatpak
|
||||
path: ${{ github.workspace }}/release-linux-flatpak
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
||||
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
||||
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
||||
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||
${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
|
||||
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
|
||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
||||
|
||||
# https://git-scm.com/docs/git-tag - git-tag Docu
|
||||
#
|
||||
# git tag - lists all local tags
|
||||
# git tag -d 1.0.19-1 - delete local tag
|
||||
#
|
||||
# git ls-remote --tags - lists all remote tags
|
||||
# git push origin --delete refs/tags/1.0.19-1 - delete remote tag
|
||||
|
3
.github/workflows/codacy-code-reporter.yml
vendored
3
.github/workflows/codacy-code-reporter.yml
vendored
@ -7,8 +7,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'haveno-dex/haveno'
|
||||
name: Publish coverage
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -18,7 +18,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -68,4 +68,4 @@ jobs:
|
||||
run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
6
.github/workflows/label.yml
vendored
6
.github/workflows/label.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
issueLabeled:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Bounty explanation
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
@ -16,10 +16,10 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: >
|
||||
There is a bounty on this issue, the amount is in the title. The reward will be awarded to the first person or group of people who resolves this issue.
|
||||
There is a bounty on this issue. The amount is in the title. The reward will be awarded to the first person or group of people whose solution is accepted and merged.
|
||||
|
||||
|
||||
If you are starting to work on this bounty, please write a comment, so that we can assign the issue to you. We expect contributors to provide a PR in a reasonable time frame or, in case of an extensive work, updates on their progresses. We will unassign the issue if we feel the assignee is not responsive or has abandoned the task.
|
||||
In some cases, we may assign the issue to specific contributors. We expect contributors to provide a PR in a reasonable time frame or, in case of an extensive work, updates on their progress. We will unassign the issue if we feel the assignee is not responsive or has abandoned the task.
|
||||
|
||||
|
||||
Read the [full conditions and details](https://github.com/haveno-dex/haveno/blob/master/docs/bounties.md) of our bounty system.
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,3 +37,5 @@ deploy
|
||||
.vscode
|
||||
.vim/*
|
||||
*/.factorypath
|
||||
.flatpak-builder
|
||||
exchange.haveno.Haveno.yaml
|
||||
|
6
LICENSE
6
LICENSE
@ -1,7 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2020 Haveno Dex
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@ -659,4 +659,4 @@ specific requirements.
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
161
Makefile
161
Makefile
@ -28,7 +28,7 @@ haveno-apps:
|
||||
refresh-deps:
|
||||
./gradlew --write-verification-metadata sha256 && ./gradlew build --refresh-keys --refresh-dependencies -x test -x checkstyleMain -x checkstyleTest
|
||||
|
||||
deploy:
|
||||
deploy-screen:
|
||||
# create a new screen session named 'localnet'
|
||||
screen -dmS localnet
|
||||
# deploy each node in its own named screen window
|
||||
@ -40,23 +40,21 @@ deploy:
|
||||
screen -S localnet -X screen -t $$target; \
|
||||
screen -S localnet -p $$target -X stuff "make $$target\n"; \
|
||||
done;
|
||||
# give bitcoind rpc server time to start
|
||||
# give time to start
|
||||
sleep 5
|
||||
|
||||
bitcoind:
|
||||
./.localnet/bitcoind \
|
||||
-regtest \
|
||||
-peerbloomfilters=1 \
|
||||
-datadir=.localnet/ \
|
||||
-rpcuser=haveno \
|
||||
-rpcpassword=1234 \
|
||||
|
||||
btc-blocks:
|
||||
./.localnet/bitcoin-cli \
|
||||
-regtest \
|
||||
-rpcuser=haveno \
|
||||
-rpcpassword=1234 \
|
||||
generatetoaddress 101 bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz
|
||||
deploy-tmux:
|
||||
# Start a new tmux session named 'localnet' (detached)
|
||||
tmux new-session -d -s localnet -n main "make seednode-local"
|
||||
# Split the window into panes and run each node in its own pane
|
||||
tmux split-window -h -t localnet "make user1-desktop-local" # Split horizontally for user1
|
||||
tmux split-window -v -t localnet:0.0 "make user2-desktop-local" # Split vertically on the left for user2
|
||||
tmux split-window -v -t localnet:0.1 "make arbitrator-desktop-local" # Split vertically on the right for arbitrator
|
||||
tmux select-layout -t localnet tiled
|
||||
# give time to start
|
||||
sleep 5
|
||||
# Attach to the tmux session
|
||||
tmux attach-session -t localnet
|
||||
|
||||
.PHONY: build seednode localnet
|
||||
|
||||
@ -72,9 +70,11 @@ monerod1-local:
|
||||
--log-level 0 \
|
||||
--add-exclusive-node 127.0.0.1:48080 \
|
||||
--add-exclusive-node 127.0.0.1:58080 \
|
||||
--max-connections-per-ip 10 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--fixed-difficulty 500 \
|
||||
--disable-rpc-ban \
|
||||
--rpc-max-connections-per-private-ip 100 \
|
||||
|
||||
monerod2-local:
|
||||
./.localnet/monerod \
|
||||
@ -90,9 +90,11 @@ monerod2-local:
|
||||
--confirm-external-bind \
|
||||
--add-exclusive-node 127.0.0.1:28080 \
|
||||
--add-exclusive-node 127.0.0.1:58080 \
|
||||
--max-connections-per-ip 10 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--fixed-difficulty 500 \
|
||||
--disable-rpc-ban \
|
||||
--rpc-max-connections-per-private-ip 100 \
|
||||
|
||||
monerod3-local:
|
||||
./.localnet/monerod \
|
||||
@ -108,19 +110,11 @@ monerod3-local:
|
||||
--confirm-external-bind \
|
||||
--add-exclusive-node 127.0.0.1:28080 \
|
||||
--add-exclusive-node 127.0.0.1:48080 \
|
||||
--max-connections-per-ip 10 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--fixed-difficulty 500 \
|
||||
--disable-rpc-ban \
|
||||
|
||||
funding-wallet-stagenet:
|
||||
./.localnet/monero-wallet-rpc \
|
||||
--stagenet \
|
||||
--rpc-bind-port 18084 \
|
||||
--rpc-login rpc_user:abc123 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--wallet-dir ./.localnet \
|
||||
--daemon-ssl-allow-any-cert \
|
||||
--daemon-address http://127.0.0.1:38081 \
|
||||
--rpc-max-connections-per-private-ip 100 \
|
||||
|
||||
#--proxy 127.0.0.1:49775 \
|
||||
|
||||
@ -133,6 +127,23 @@ funding-wallet-local:
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--wallet-dir ./.localnet \
|
||||
|
||||
funding-wallet-stagenet:
|
||||
./.localnet/monero-wallet-rpc \
|
||||
--stagenet \
|
||||
--rpc-bind-port 38084 \
|
||||
--rpc-login rpc_user:abc123 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--wallet-dir ./.localnet \
|
||||
--daemon-ssl-allow-any-cert \
|
||||
--daemon-address http://127.0.0.1:38081 \
|
||||
|
||||
funding-wallet-mainnet:
|
||||
./.localnet/monero-wallet-rpc \
|
||||
--rpc-bind-port 18084 \
|
||||
--rpc-login rpc_user:abc123 \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--wallet-dir ./.localnet \
|
||||
|
||||
# use .bat extension for windows binaries
|
||||
APP_EXT :=
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@ -304,7 +315,7 @@ seednode-stagenet:
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--nodePort=3002 \
|
||||
--appName=haveno-XMR_STAGENET_Seed_3002 \
|
||||
--xmrNode=http://127.0.0.1:38081 \
|
||||
|
||||
@ -313,7 +324,7 @@ seednode2-stagenet:
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--nodePort=3003 \
|
||||
--appName=haveno-XMR_STAGENET_Seed_3003 \
|
||||
--xmrNode=http://127.0.0.1:38081 \
|
||||
|
||||
@ -412,6 +423,17 @@ haveno-desktop-stagenet:
|
||||
--apiPort=3204 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
haveno-daemon-stagenet:
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--appName=Haveno \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=3204 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
# Mainnet network
|
||||
|
||||
monerod:
|
||||
@ -424,7 +446,7 @@ seednode:
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--nodePort=1002 \
|
||||
--appName=haveno-XMR_MAINNET_Seed_1002 \
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
|
||||
@ -433,11 +455,11 @@ seednode2:
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--nodePort=1003 \
|
||||
--appName=haveno-XMR_MAINNET_Seed_1003 \
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
|
||||
arbitrator-daemon:
|
||||
arbitrator-daemon-mainnet:
|
||||
# Arbitrator needs to be registered before making trades
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
@ -451,8 +473,7 @@ arbitrator-daemon:
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
# Arbitrator needs to be registered before making trades
|
||||
arbitrator-desktop:
|
||||
arbitrator-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -464,7 +485,56 @@ arbitrator-desktop:
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
user1-daemon:
|
||||
arbitrator2-daemon-mainnet:
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_arbitrator2 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1205 \
|
||||
--passwordRequired=false \
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
arbitrator2-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_arbitrator2 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1205 \
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
--useNativeXmrWallet=false \
|
||||
|
||||
haveno-daemon-mainnet:
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--appName=Haveno \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1201 \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
haveno-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
--useDevPrivilegeKeys=false \
|
||||
--nodePort=9999 \
|
||||
--appName=Haveno \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1201 \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
user1-daemon-mainnet:
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -472,11 +542,12 @@ user1-daemon:
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_user1 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1201 \
|
||||
--apiPort=1202 \
|
||||
--passwordRequired=false \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
user1-desktop:
|
||||
user1-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -484,10 +555,11 @@ user1-desktop:
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_user1 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1201 \
|
||||
--apiPort=1202 \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
user2-daemon:
|
||||
user2-daemon-mainnet:
|
||||
./haveno-daemon$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -495,11 +567,12 @@ user2-daemon:
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_user2 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1202 \
|
||||
--apiPort=1203 \
|
||||
--passwordRequired=false \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
user2-desktop:
|
||||
user2-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -507,10 +580,11 @@ user2-desktop:
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_user2 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1202 \
|
||||
--apiPort=1203 \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
||||
user3-desktop:
|
||||
user3-desktop-mainnet:
|
||||
./haveno-desktop$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
--useLocalhostForP2P=false \
|
||||
@ -518,5 +592,6 @@ user3-desktop:
|
||||
--nodePort=9999 \
|
||||
--appName=haveno-XMR_MAINNET_user3 \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=1203 \
|
||||
--apiPort=1204 \
|
||||
--useNativeXmrWallet=false \
|
||||
--ignoreLocalXmrNode=false \
|
||||
|
67
README.md
67
README.md
@ -1,86 +1,85 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
|
||||
|
||||
[](https://app.codacy.com/gh/haveno-dex/haveno/dashboard)
|
||||

|
||||
[](https://github.com/orgs/haveno-dex/projects/2) |
|
||||
[](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
|
||||
[](https://twitter.com/havenodex)
|
||||
[](https://matrix.to/#/#haveno:monero.social) [](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
|
||||
</div>
|
||||
|
||||
## What is Haveno?
|
||||
|
||||
Haveno (pronounced ha‧ve‧no) is a platform for people who want to exchange [Monero](https://getmonero.org) for fiat currencies like EUR, GBP, and USD or other cryptocurrencies like BTC, ETH, and BCH.
|
||||
Haveno (pronounced ha‧ve‧no) is an open source platform to exchange [Monero](https://getmonero.org) for fiat currencies like USD, EUR, and GBP or other cryptocurrencies like BTC, ETH, and BCH.
|
||||
|
||||
Main features:
|
||||
|
||||
- All communications are routed through **Tor**, to preserve your privacy
|
||||
- Communications are routed through **Tor**, to preserve your privacy.
|
||||
|
||||
- Trades are **peer-to-peer**: trades on Haveno will happen between people only, there is no central authority.
|
||||
- Trades are **peer-to-peer**: trades on Haveno happen between people only, there is no central authority.
|
||||
|
||||
- Trades are **non-custodial**: Haveno provides arbitration in case something goes wrong during the trade, but we will never have access to your funds.
|
||||
- Trades are **non-custodial**: Haveno supports arbitration in case something goes wrong during the trade, but arbitrators never have access to your funds.
|
||||
|
||||
- There is **No token**, because we don't need it. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
|
||||
|
||||
- The revenue generated by Haveno will be managed by an entity called Council (more info soon), composed by members of the Monero/Haveno community, not the Haveno Core Team and will be used to **fund Haveno and Monero** development.
|
||||
- There is **No token**, because it's not needed. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
|
||||
|
||||
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
|
||||
|
||||
## Status of the project
|
||||
## Haveno Demo
|
||||
|
||||
A live test network is online and users can already run Haveno and make test trades between each others using Monero's stagenet. See the [instructions to build Haveno and connect to the network](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md). Note that Haveno is still very much in development. If you find issues or bugs, please let us know.
|
||||
https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47
|
||||
|
||||
Main repositories:
|
||||
## Installing Haveno
|
||||
|
||||
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network.
|
||||
|
||||
> [!note]
|
||||
> The official Haveno repository does not support making real trades directly.
|
||||
>
|
||||
> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time.
|
||||
|
||||
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network.
|
||||
|
||||
Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md).
|
||||
|
||||
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
|
||||
|
||||
## Main repositories
|
||||
|
||||
- **[haveno](https://github.com/haveno-dex/haveno)** - This repository. The core of Haveno.
|
||||
- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - The user interface.
|
||||
- **[haveno-ts](https://github.com/haveno-dex/haveno-ts)** - TypeScript library for using Haveno.
|
||||
- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - A new user interface (WIP).
|
||||
- **[haveno-meta](https://github.com/haveno-dex/haveno-meta)** - For project-wide discussions and proposals.
|
||||
|
||||
If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties)
|
||||
|
||||
The PGP keys of the core team members are in `gpg_keys/`.
|
||||
If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties).
|
||||
|
||||
## Keep in touch and help out!
|
||||
|
||||
Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server:
|
||||
|
||||
- General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`)
|
||||
- Development discussions: **Haveno Development** ([#haveno-dev:monero.social](https://matrix.to/#/#haveno-dev:monero.social)) relayed on IRC/Libera (`#haveno-dev`)
|
||||
- Development discussions: **Haveno Development** ([#haveno-development:monero.social](https://matrix.to/#/#haveno-development:monero.social)) relayed on IRC/Libera (`#haveno-development`)
|
||||
|
||||
Email: contact@haveno.exchange
|
||||
Website: [haveno.exchange](https://haveno.exchange)
|
||||
|
||||
## Running a local Haveno test network
|
||||
|
||||
See [docs/installing.md](docs/installing.md)
|
||||
|
||||
## Contributing to Haveno
|
||||
|
||||
See the [developer guide](docs/developer-guide.md) to get started developing for Haveno.
|
||||
|
||||
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides.
|
||||
|
||||
If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties.
|
||||
If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties.
|
||||
|
||||
## Bounties
|
||||
|
||||
To incentivize development and reward contributors we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the issues eligible for a bounty on the [dedicated Kanban board](https://github.com/orgs/haveno-dex/projects/2) or look for [issues labelled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue+is%3Aopen+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
|
||||
To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
|
||||
|
||||
## Support and sponsorships
|
||||
|
||||
To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project:
|
||||
|
||||
### Monero
|
||||
|
||||
`42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F`
|
||||
|
||||
<!--  -->
|
||||
<p>
|
||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
|
||||
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
|
||||
</p>
|
||||
|
||||
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
|
||||
|
||||
### Bitcoin
|
||||
|
||||
`1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ`
|
||||
|
||||
<!--  -->
|
||||
|
@ -78,7 +78,7 @@ public class ApiTestMain {
|
||||
|
||||
} catch (Throwable ex) {
|
||||
err.println("Fault: An unexpected error occurred. " +
|
||||
"Please file a report at https://haveno.exchange/issues");
|
||||
"Please file a report at https://github.com/haveno-dex/haveno/issues");
|
||||
ex.printStackTrace(err);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import java.util.stream.Collectors;
|
||||
import static haveno.apitest.config.ApiTestConfig.BTC;
|
||||
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
|
||||
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
|
||||
import static java.lang.String.format;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Arrays.stream;
|
||||
@ -157,8 +157,8 @@ public class MethodTest extends ApiTestCase {
|
||||
return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
|
||||
}
|
||||
|
||||
public static final Supplier<Double> defaultBuyerSecurityDepositPct = () -> {
|
||||
var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent());
|
||||
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
|
||||
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
|
||||
if (defaultPct.precision() != 2)
|
||||
throw new IllegalStateException(format(
|
||||
"Unexpected decimal precision, expected 2 but actual is %d%n."
|
||||
|
@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
|
||||
10000000L,
|
||||
10000000L,
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
paymentAccountId,
|
||||
NO_TRIGGER_PRICE);
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
10_000_000L,
|
||||
"36000",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
audAccount.getId());
|
||||
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
10_000_000L,
|
||||
"30000.1234",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
usdAccount.getId());
|
||||
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
5_000_000L,
|
||||
"29500.1234",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
eurAccount.getId());
|
||||
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
|
@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
10_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
usdAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
||||
@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
10_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
nzdAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
||||
@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
5_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
gbpAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
||||
@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
5_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
brlAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
|
||||
@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
10_000_000L,
|
||||
5_000_000L,
|
||||
0.0,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
usdAccount.getId(),
|
||||
triggerPrice);
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
|
@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
||||
100_000_000L,
|
||||
75_000_000L,
|
||||
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId());
|
||||
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
||||
100_000_000L,
|
||||
50_000_000L,
|
||||
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId());
|
||||
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||
assertTrue(newOffer.getIsMyOffer());
|
||||
@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
||||
100_000_000L,
|
||||
75_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId(),
|
||||
triggerPrice);
|
||||
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||
@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
||||
100_000_000L,
|
||||
50_000_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||
|
@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
||||
100000000000L, // exceeds amount limit
|
||||
100000000000L,
|
||||
"10000.0000",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
usdAccount.getId()));
|
||||
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
|
||||
}
|
||||
@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
||||
10000000L,
|
||||
10000000L,
|
||||
"40000.0000",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
chfAccount.getId()));
|
||||
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
|
||||
assertEquals(expectedError, exception.getMessage());
|
||||
@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
||||
10000000L,
|
||||
10000000L,
|
||||
"63000.0000",
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
audAccount.getId()));
|
||||
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
|
||||
assertEquals(expectedError, exception.getMessage());
|
||||
|
@ -676,7 +676,7 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
|
||||
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SELECTED_TRADE_CURRENCY),
|
||||
paymentAccount.getSelectedTradeCurrency().getCode());
|
||||
verifyCommonFormEntries(paymentAccount);
|
||||
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
|
||||
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUsername());
|
||||
print(paymentAccount);
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
|
||||
12_500_000L,
|
||||
12_500_000L, // min-amount = amount
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesUsdAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
var offerId = alicesOffer.getId();
|
||||
|
@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
|
||||
1_000_000L,
|
||||
1_000_000L, // min-amount = amount
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesPaymentAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
var offerId = alicesOffer.getId();
|
||||
|
@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
|
||||
15_000_000L,
|
||||
7_500_000L,
|
||||
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId());
|
||||
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
||||
genBtcBlocksThenWait(1, 5000);
|
||||
|
@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
||||
12_500_000L,
|
||||
12_500_000L, // min-amount = amount
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesUsdAccount.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
var offerId = alicesOffer.getId();
|
||||
|
@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
|
||||
20_000_000L,
|
||||
10_500_000L,
|
||||
priceMarginPctInput,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
alicesXmrAcct.getId(),
|
||||
NO_TRIGGER_PRICE);
|
||||
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
||||
|
@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
paymentAcct.getId(),
|
||||
triggerPrice);
|
||||
log.info("SELL offer {} created with margin based price {}.",
|
||||
@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
paymentAcct.getId(),
|
||||
triggerPrice);
|
||||
log.info("BUY offer {} created with margin based price {}.",
|
||||
|
@ -28,7 +28,7 @@ import java.text.DecimalFormat;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
|
||||
import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct;
|
||||
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
|
||||
import static haveno.cli.CurrencyFormat.formatSatoshis;
|
||||
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
|
||||
@ -119,7 +119,7 @@ public class RandomOffer {
|
||||
amount,
|
||||
minAmount,
|
||||
priceMargin,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
"0" /*no trigger price*/);
|
||||
} else {
|
||||
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
|
||||
@ -128,7 +128,7 @@ public class RandomOffer {
|
||||
amount,
|
||||
minAmount,
|
||||
fixedOfferPrice,
|
||||
defaultBuyerSecurityDepositPct.get());
|
||||
defaultSecurityDepositPct.get());
|
||||
}
|
||||
this.id = offer.getId();
|
||||
return this;
|
||||
|
29
assets/src/main/java/haveno/asset/Trc20Token.java
Normal file
29
assets/src/main/java/haveno/asset/Trc20Token.java
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.asset;
|
||||
|
||||
/**
|
||||
* Abstract base class for Tron-based {@link Token}s that implement the
|
||||
* TRC-20 Token Standard.
|
||||
*/
|
||||
public abstract class Trc20Token extends Token {
|
||||
|
||||
public Trc20Token(String name, String tickerSymbol) {
|
||||
super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}"));
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@
|
||||
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
|
||||
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
|
||||
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
|
||||
* {@link haveno.asset.tokens.DaiStablecoin}.
|
||||
* {@link haveno.asset.tokens.DaiStablecoinERC20}.
|
||||
* <p>
|
||||
* The purpose of this package is to provide everything necessary for registering
|
||||
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno
|
||||
|
@ -19,9 +19,9 @@ package haveno.asset.tokens;
|
||||
|
||||
import haveno.asset.Erc20Token;
|
||||
|
||||
public class USDCoin extends Erc20Token {
|
||||
public class DaiStablecoinERC20 extends Erc20Token {
|
||||
|
||||
public USDCoin() {
|
||||
super("USD Coin", "USDC");
|
||||
public DaiStablecoinERC20() {
|
||||
super("Dai Stablecoin", "DAI-ERC20");
|
||||
}
|
||||
}
|
11
assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
Normal file
11
assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
Normal file
@ -0,0 +1,11 @@
|
||||
package haveno.asset.tokens;
|
||||
|
||||
import haveno.asset.Erc20Token;
|
||||
|
||||
public class TetherUSDERC20 extends Erc20Token {
|
||||
public TetherUSDERC20() {
|
||||
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
||||
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
||||
super("Tether USD (ERC20)", "USDT-ERC20");
|
||||
}
|
||||
}
|
11
assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
Normal file
11
assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
Normal file
@ -0,0 +1,11 @@
|
||||
package haveno.asset.tokens;
|
||||
|
||||
import haveno.asset.Trc20Token;
|
||||
|
||||
public class TetherUSDTRC20 extends Trc20Token {
|
||||
public TetherUSDTRC20() {
|
||||
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
||||
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
||||
super("Tether USD (TRC20)", "USDT-TRC20");
|
||||
}
|
||||
}
|
@ -19,9 +19,9 @@ package haveno.asset.tokens;
|
||||
|
||||
import haveno.asset.Erc20Token;
|
||||
|
||||
public class DaiStablecoin extends Erc20Token {
|
||||
public class USDCoinERC20 extends Erc20Token {
|
||||
|
||||
public DaiStablecoin() {
|
||||
super("Dai Stablecoin", "DAI");
|
||||
public USDCoinERC20() {
|
||||
super("USD Coin (ERC20)", "USDC-ERC20");
|
||||
}
|
||||
}
|
@ -7,3 +7,7 @@ haveno.asset.coins.BitcoinCash
|
||||
haveno.asset.coins.Ether
|
||||
haveno.asset.coins.Litecoin
|
||||
haveno.asset.coins.Monero
|
||||
haveno.asset.tokens.TetherUSDERC20
|
||||
haveno.asset.tokens.TetherUSDTRC20
|
||||
haveno.asset.tokens.USDCoinERC20
|
||||
haveno.asset.tokens.DaiStablecoinERC20
|
@ -32,6 +32,7 @@ public class BitcoinTest extends AbstractAssetTest {
|
||||
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
|
||||
assertValidAddress("1111111111111111111114oLvT2");
|
||||
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
|
||||
assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.asset.coins;
|
||||
|
||||
import haveno.asset.AbstractAssetTest;
|
||||
import haveno.asset.tokens.TetherUSDERC20;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TetherUSDERC20Test extends AbstractAssetTest {
|
||||
|
||||
public TetherUSDERC20Test() {
|
||||
super(new TetherUSDERC20());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidAddresses() {
|
||||
assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226");
|
||||
assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAddresses() {
|
||||
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
|
||||
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
|
||||
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.asset.coins;
|
||||
|
||||
import haveno.asset.AbstractAssetTest;
|
||||
import haveno.asset.tokens.TetherUSDTRC20;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TetherUSDTRC20Test extends AbstractAssetTest {
|
||||
|
||||
public TetherUSDTRC20Test() {
|
||||
super(new TetherUSDTRC20());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidAddresses() {
|
||||
assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAddresses() {
|
||||
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
|
||||
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
|
||||
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
|
||||
}
|
||||
}
|
96
build.gradle
96
build.gradle
@ -49,7 +49,7 @@ configure(subprojects) {
|
||||
gsonVersion = '2.8.5'
|
||||
guavaVersion = '32.1.1-jre'
|
||||
guiceVersion = '7.0.0'
|
||||
moneroJavaVersion = '0.8.22'
|
||||
moneroJavaVersion = '0.8.36'
|
||||
httpclient5Version = '5.0'
|
||||
hamcrestVersion = '2.2'
|
||||
httpclientVersion = '4.5.12'
|
||||
@ -71,7 +71,7 @@ configure(subprojects) {
|
||||
loggingVersion = '1.2'
|
||||
lombokVersion = '1.18.30'
|
||||
mockitoVersion = '5.10.0'
|
||||
netlayerVersion = '33485b9e57' // Netlayer version "0.7.7" with Tor browser version 13.0.6 and tor binary version: 0.4.8.9
|
||||
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
|
||||
protobufVersion = '3.19.1'
|
||||
protocVersion = protobufVersion
|
||||
pushyVersion = '0.13.2'
|
||||
@ -163,28 +163,33 @@ configure([project(':cli'),
|
||||
|
||||
// edit generated shell scripts such that they expect to be executed in the
|
||||
// project root dir as opposed to a 'bin' subdirectory
|
||||
def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat")
|
||||
windowsScriptFile.text = windowsScriptFile.text.replace(
|
||||
'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%')
|
||||
|
||||
def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName")
|
||||
unixScriptFile.text = unixScriptFile.text.replace(
|
||||
'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit')
|
||||
|
||||
if (applicationName == 'desktop') {
|
||||
if (osdetector.os == 'windows') {
|
||||
def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat")
|
||||
windowsScriptFile.text = windowsScriptFile.text.replace(
|
||||
'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
|
||||
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED')
|
||||
'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%')
|
||||
|
||||
if (applicationName == 'desktop') {
|
||||
windowsScriptFile.text = windowsScriptFile.text.replace(
|
||||
'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
|
||||
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED')
|
||||
}
|
||||
}
|
||||
else {
|
||||
def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName")
|
||||
unixScriptFile.text = unixScriptFile.text.replace(
|
||||
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
|
||||
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"')
|
||||
'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit')
|
||||
|
||||
if (applicationName == 'desktop') {
|
||||
unixScriptFile.text = unixScriptFile.text.replace(
|
||||
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' +
|
||||
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' +
|
||||
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"')
|
||||
}
|
||||
}
|
||||
|
||||
if (applicationName == 'apitest') {
|
||||
@ -312,9 +317,8 @@ configure(project(':common')) {
|
||||
exclude(module: 'animal-sniffer-annotations')
|
||||
}
|
||||
|
||||
// override transitive dependency version from 1.5 to the same version just identified by commit number.
|
||||
// Remove this if transitive dependency is changed to something else than 1.5
|
||||
implementation(group: 'com.github.JesusMcCloud', name: 'jtorctl') { version { strictly "[9b5ba2036b]" } }
|
||||
// override transitive dependency and use latest version from bisq
|
||||
implementation(group: 'com.github.bisq-network', name: 'jtorctl') { version { strictly "[b2a172f44edcd8deaa5ed75d936dcbb007f0d774]" } }
|
||||
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
|
||||
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
|
||||
}
|
||||
@ -330,6 +334,7 @@ configure(project(':p2p')) {
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "org.fxmisc.easybind:easybind:$easybindVersion"
|
||||
implementation "org.slf4j:slf4j-api:$slf4jVersion"
|
||||
implementation "org.apache.commons:commons-lang3:$langVersion"
|
||||
implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") {
|
||||
exclude(module: 'slf4j-api')
|
||||
}
|
||||
@ -360,6 +365,7 @@ configure(project(':p2p')) {
|
||||
testImplementation "ch.qos.logback:logback-core:$logbackVersion"
|
||||
testImplementation "org.apache.commons:commons-lang3:$langVersion"
|
||||
testImplementation("org.mockito:mockito-core:$mockitoVersion")
|
||||
testImplementation("org.mockito:mockito-junit-jupiter:$mockitoVersion")
|
||||
|
||||
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
|
||||
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
|
||||
@ -451,14 +457,14 @@ configure(project(':core')) {
|
||||
doLast {
|
||||
// get monero binaries download url
|
||||
Map moneroBinaries = [
|
||||
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release2/monero-bins-haveno-linux.tar.gz',
|
||||
'linux-x86_64-sha256' : '3537fe2006997a1065748d27e9513ac3e0c942ab56a97a6e43065ddfd1820394',
|
||||
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release2/monero-bins-haveno-linux-aarch64.tar.gz',
|
||||
'linux-aarch64-sha256' : '6ff81c61780fe08defbd6576bd93c6711cf5ad3e79be0e3bc2184ff11cc6a472',
|
||||
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release2/monero-bins-haveno-mac.tar.gz',
|
||||
'mac-sha256' : 'c7cafe1000a5839f02d02ed2edce5b1df3a06b5c77f4d91eaba106d948347730',
|
||||
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release2/monero-bins-haveno-windows.zip',
|
||||
'windows-sha256' : '9b900faefa75f354870646989484978d1fb11add392ffd05eb5abe7e514e395a'
|
||||
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz',
|
||||
'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
|
||||
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
|
||||
'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
|
||||
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
|
||||
'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
|
||||
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
|
||||
'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
|
||||
]
|
||||
|
||||
String osKey
|
||||
@ -500,16 +506,16 @@ configure(project(':core')) {
|
||||
} else {
|
||||
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
|
||||
}
|
||||
}
|
||||
|
||||
// add the current platform's monero dependencies into the resources folder for installation
|
||||
copy {
|
||||
from "${monerodFile}"
|
||||
into "${project(':core').projectDir}/src/main/resources/bin"
|
||||
}
|
||||
copy {
|
||||
from "${moneroRpcFile}"
|
||||
into "${project(':core').projectDir}/src/main/resources/bin"
|
||||
}
|
||||
// add the current platform's monero dependencies into the resources folder for installation
|
||||
copy {
|
||||
from "${monerodFile}"
|
||||
into "${project(':core').projectDir}/src/main/resources/bin"
|
||||
}
|
||||
copy {
|
||||
from "${moneroRpcFile}"
|
||||
into "${project(':core').projectDir}/src/main/resources/bin"
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,6 +537,7 @@ configure(project(':core')) {
|
||||
|
||||
ext.downloadAndVerifyDependencies = { String archiveURL, String archiveSHA256, File destinationArchiveFile ->
|
||||
ext.dependencyDownloadedAndVerified = false
|
||||
|
||||
// if archive exists, check to see if its already up to date
|
||||
if (destinationArchiveFile.exists()) {
|
||||
println "Verifying existing archive ${destinationArchiveFile}"
|
||||
@ -544,14 +551,15 @@ configure(project(':core')) {
|
||||
}
|
||||
}
|
||||
|
||||
// download archives
|
||||
println "Downloading ${archiveURL}"
|
||||
ant.get(src: archiveURL, dest: destinationArchiveFile)
|
||||
println 'Download saved to ' + destinationArchiveFile
|
||||
|
||||
// verify checksum
|
||||
println 'Verifying checksum for downloaded binary ...'
|
||||
ant.archiveHash = archiveSHA256
|
||||
// use a different verifyProperty name from existing verification or it will always fail
|
||||
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'downloadedHashMatches')
|
||||
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'downloadedHashMatches') // use a different verifyProperty name from existing verification or it will always fail
|
||||
if (ant.properties['downloadedHashMatches'] != 'true') {
|
||||
ant.fail('Checksum mismatch: Downloaded archive has a different checksum than expected')
|
||||
}
|
||||
@ -602,7 +610,7 @@ configure(project(':desktop')) {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
apply from: 'package/package.gradle'
|
||||
|
||||
version = '1.0.0-SNAPSHOT'
|
||||
version = '1.1.0-SNAPSHOT'
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
|
@ -81,7 +81,7 @@ public class OffersServiceRequest {
|
||||
.setUseMarketBasedPrice(useMarketBasedPrice)
|
||||
.setPrice(fixedPrice)
|
||||
.setMarketPriceMarginPct(marketPriceMarginPct)
|
||||
.setBuyerSecurityDepositPct(securityDepositPct)
|
||||
.setSecurityDepositPct(securityDepositPct)
|
||||
.setPaymentAccountId(paymentAcctId)
|
||||
.setTriggerPrice(triggerPrice)
|
||||
.build();
|
||||
|
@ -69,7 +69,7 @@ public class ClockWatcher {
|
||||
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
|
||||
|
||||
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
|
||||
log.info("We have been in standby mode for {} sec", missedMs / 1000);
|
||||
log.warn("We have been in standby mode for {} sec", missedMs / 1000);
|
||||
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ public class ThreadUtils {
|
||||
synchronized (THREADS) {
|
||||
THREADS.put(threadId, Thread.currentThread());
|
||||
}
|
||||
Thread.currentThread().setName(threadId);
|
||||
command.run();
|
||||
});
|
||||
}
|
||||
|
@ -59,8 +59,10 @@ public class Capabilities {
|
||||
}
|
||||
|
||||
public Capabilities(Collection<Capability> capabilities) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.addAll(capabilities);
|
||||
synchronized (capabilities) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.addAll(capabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,9 +75,11 @@ public class Capabilities {
|
||||
}
|
||||
|
||||
public void set(Collection<Capability> capabilities) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.clear();
|
||||
this.capabilities.addAll(capabilities);
|
||||
synchronized (capabilities) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.clear();
|
||||
this.capabilities.addAll(capabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,15 +91,19 @@ public class Capabilities {
|
||||
|
||||
public void addAll(Capabilities capabilities) {
|
||||
if (capabilities != null) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.addAll(capabilities.capabilities);
|
||||
synchronized (capabilities.capabilities) {
|
||||
synchronized (this.capabilities) {
|
||||
this.capabilities.addAll(capabilities.capabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsAll(final Set<Capability> requiredItems) {
|
||||
synchronized (this.capabilities) {
|
||||
return capabilities.containsAll(requiredItems);
|
||||
synchronized(requiredItems) {
|
||||
synchronized (this.capabilities) {
|
||||
return capabilities.containsAll(requiredItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +137,9 @@ public class Capabilities {
|
||||
* @return int list of Capability ordinals
|
||||
*/
|
||||
public static List<Integer> toIntList(Capabilities capabilities) {
|
||||
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
|
||||
synchronized (capabilities.capabilities) {
|
||||
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,11 +149,13 @@ public class Capabilities {
|
||||
* @return a {@link Capabilities} object
|
||||
*/
|
||||
public static Capabilities fromIntList(List<Integer> capabilities) {
|
||||
return new Capabilities(capabilities.stream()
|
||||
.filter(integer -> integer < Capability.values().length)
|
||||
.filter(integer -> integer >= 0)
|
||||
.map(integer -> Capability.values()[integer])
|
||||
.collect(Collectors.toSet()));
|
||||
synchronized (capabilities) {
|
||||
return new Capabilities(capabilities.stream()
|
||||
.filter(integer -> integer < Capability.values().length)
|
||||
.filter(integer -> integer >= 0)
|
||||
.map(integer -> Capability.values()[integer])
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -181,7 +193,9 @@ public class Capabilities {
|
||||
}
|
||||
|
||||
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
|
||||
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
|
||||
synchronized (capabilities.capabilities) {
|
||||
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -211,8 +225,10 @@ public class Capabilities {
|
||||
// Neither would support removal of past capabilities, a use case we never had so far and which might have
|
||||
// backward compatibility issues, so we should treat capabilities as an append-only data structure.
|
||||
public int findHighestCapability(Capabilities capabilities) {
|
||||
return (int) capabilities.capabilities.stream()
|
||||
.mapToLong(e -> (long) e.ordinal())
|
||||
.sum();
|
||||
synchronized (capabilities.capabilities) {
|
||||
return (int) capabilities.capabilities.stream()
|
||||
.mapToLong(e -> (long) e.ordinal())
|
||||
.sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
import ch.qos.logback.classic.filter.ThresholdFilter;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
|
||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
||||
@ -52,11 +53,12 @@ public class Log {
|
||||
|
||||
SizeBasedTriggeringPolicy<ILoggingEvent> triggeringPolicy = new SizeBasedTriggeringPolicy<>();
|
||||
triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB"));
|
||||
triggeringPolicy.setContext(loggerContext);
|
||||
triggeringPolicy.start();
|
||||
|
||||
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
|
||||
encoder.setContext(loggerContext);
|
||||
encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n");
|
||||
encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg%n");
|
||||
encoder.start();
|
||||
|
||||
appender.setEncoder(encoder);
|
||||
@ -64,25 +66,43 @@ public class Log {
|
||||
appender.setTriggeringPolicy(triggeringPolicy);
|
||||
appender.start();
|
||||
|
||||
logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
|
||||
logbackLogger.addAppender(appender);
|
||||
logbackLogger.setLevel(Level.INFO);
|
||||
|
||||
// log errors in separate file
|
||||
// not working as expected still.... damn logback...
|
||||
/* FileAppender errorAppender = new FileAppender();
|
||||
errorAppender.setEncoder(encoder);
|
||||
PatternLayoutEncoder errorEncoder = new PatternLayoutEncoder();
|
||||
errorEncoder.setContext(loggerContext);
|
||||
errorEncoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger: %msg%n%ex");
|
||||
errorEncoder.start();
|
||||
|
||||
RollingFileAppender<ILoggingEvent> errorAppender = new RollingFileAppender<>();
|
||||
errorAppender.setEncoder(errorEncoder);
|
||||
errorAppender.setName("Error");
|
||||
errorAppender.setContext(loggerContext);
|
||||
errorAppender.setFile(fileName + "_error.log");
|
||||
LevelFilter levelFilter = new LevelFilter();
|
||||
levelFilter.setLevel(Level.ERROR);
|
||||
levelFilter.setOnMatch(FilterReply.ACCEPT);
|
||||
levelFilter.setOnMismatch(FilterReply.DENY);
|
||||
levelFilter.start();
|
||||
errorAppender.addFilter(levelFilter);
|
||||
|
||||
FixedWindowRollingPolicy errorRollingPolicy = new FixedWindowRollingPolicy();
|
||||
errorRollingPolicy.setContext(loggerContext);
|
||||
errorRollingPolicy.setParent(errorAppender);
|
||||
errorRollingPolicy.setFileNamePattern(fileName + "_error_%i.log");
|
||||
errorRollingPolicy.setMinIndex(1);
|
||||
errorRollingPolicy.setMaxIndex(20);
|
||||
errorRollingPolicy.start();
|
||||
|
||||
SizeBasedTriggeringPolicy<ILoggingEvent> errorTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
|
||||
errorTriggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB"));
|
||||
errorTriggeringPolicy.start();
|
||||
|
||||
ThresholdFilter thresholdFilter = new ThresholdFilter();
|
||||
thresholdFilter.setLevel("WARN");
|
||||
thresholdFilter.start();
|
||||
|
||||
errorAppender.setRollingPolicy(errorRollingPolicy);
|
||||
errorAppender.setTriggeringPolicy(errorTriggeringPolicy);
|
||||
errorAppender.addFilter(thresholdFilter);
|
||||
errorAppender.start();
|
||||
logbackLogger.addAppender(errorAppender);*/
|
||||
|
||||
logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
|
||||
logbackLogger.addAppender(errorAppender);
|
||||
logbackLogger.addAppender(appender);
|
||||
logbackLogger.setLevel(Level.INFO);
|
||||
}
|
||||
|
||||
public static void setCustomLogLevel(String pattern, Level logLevel) {
|
||||
|
@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
public class Version {
|
||||
// The application versions
|
||||
// We use semantic versioning with major, minor and patch
|
||||
public static final String VERSION = "1.0.0";
|
||||
public static final String VERSION = "1.1.0";
|
||||
|
||||
/**
|
||||
* Holds a list of the tagged resource files for optimizing the getData requests.
|
||||
@ -72,6 +72,25 @@ public class Version {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int compare(String version1, String version2) {
|
||||
if (version1.equals(version2))
|
||||
return 0;
|
||||
else if (getMajorVersion(version1) > getMajorVersion(version2))
|
||||
return 1;
|
||||
else if (getMajorVersion(version1) < getMajorVersion(version2))
|
||||
return -1;
|
||||
else if (getMinorVersion(version1) > getMinorVersion(version2))
|
||||
return 1;
|
||||
else if (getMinorVersion(version1) < getMinorVersion(version2))
|
||||
return -1;
|
||||
else if (getPatchVersion(version1) > getPatchVersion(version2))
|
||||
return 1;
|
||||
else if (getPatchVersion(version1) < getPatchVersion(version2))
|
||||
return -1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int getSubVersion(String version, int index) {
|
||||
final String[] split = version.split("\\.");
|
||||
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
|
||||
@ -91,8 +110,9 @@ public class Version {
|
||||
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
|
||||
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
|
||||
// the Haveno app.
|
||||
// VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
|
||||
public static final int TRADE_PROTOCOL_VERSION = 1;
|
||||
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
|
||||
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
|
||||
public static final int TRADE_PROTOCOL_VERSION = 2;
|
||||
private static String p2pMessageVersion;
|
||||
|
||||
public static String getP2PMessageVersion() {
|
||||
|
@ -76,7 +76,7 @@ public enum BaseCurrencyNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
private static class XmrStageNetParams extends RegTestParams {
|
||||
private static class XmrStageNetParams extends MainNetParams {
|
||||
@Override
|
||||
public MonetaryFormat getMonetaryFormat() {
|
||||
return XMR_MONETARY_FORMAT;
|
||||
|
@ -77,6 +77,7 @@ public class Config {
|
||||
public static final String SEED_NODES = "seedNodes";
|
||||
public static final String BAN_LIST = "banList";
|
||||
public static final String NODE_PORT = "nodePort";
|
||||
public static final String HIDDEN_SERVICE_ADDRESS = "hiddenServiceAddress";
|
||||
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
|
||||
public static final String MAX_CONNECTIONS = "maxConnections";
|
||||
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
||||
@ -116,6 +117,8 @@ public class Config {
|
||||
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
|
||||
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
||||
public static final String PASSWORD_REQUIRED = "passwordRequired";
|
||||
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
|
||||
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
|
||||
|
||||
// Default values for certain options
|
||||
public static final int UNSPECIFIED_PORT = -1;
|
||||
@ -151,6 +154,7 @@ public class Config {
|
||||
public final File appDataDir;
|
||||
public final int walletRpcBindPort;
|
||||
public final int nodePort;
|
||||
public final String hiddenServiceAddress;
|
||||
public final int maxMemory;
|
||||
public final String logLevel;
|
||||
public final List<String> bannedXmrNodes;
|
||||
@ -202,6 +206,8 @@ public class Config {
|
||||
public final boolean republishMailboxEntries;
|
||||
public final boolean bypassMempoolValidation;
|
||||
public final boolean passwordRequired;
|
||||
public final boolean updateXmrBinaries;
|
||||
public final String xmrBlockchainPath;
|
||||
|
||||
// Properties derived from options but not exposed as options themselves
|
||||
public final File torDir;
|
||||
@ -286,6 +292,12 @@ public class Config {
|
||||
.ofType(Integer.class)
|
||||
.defaultsTo(9999);
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
|
||||
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
|
||||
.withRequiredArg()
|
||||
.ofType(String.class)
|
||||
.defaultsTo("");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
|
||||
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
||||
.withRequiredArg()
|
||||
@ -613,6 +625,20 @@ public class Config {
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> updateXmrBinariesOpt =
|
||||
parser.accepts(UPDATE_XMR_BINARIES,
|
||||
"Update Monero binaries if applicable")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(true);
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> xmrBlockchainPathOpt =
|
||||
parser.accepts(XMR_BLOCKCHAIN_PATH,
|
||||
"Path to Monero blockchain when using local Monero node")
|
||||
.withRequiredArg()
|
||||
.ofType(String.class)
|
||||
.defaultsTo("");
|
||||
|
||||
try {
|
||||
CompositeOptionSet options = new CompositeOptionSet();
|
||||
|
||||
@ -670,6 +696,7 @@ public class Config {
|
||||
this.helpRequested = options.has(helpOpt);
|
||||
this.configFile = configFile;
|
||||
this.nodePort = options.valueOf(nodePortOpt);
|
||||
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
|
||||
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
||||
this.maxMemory = options.valueOf(maxMemoryOpt);
|
||||
this.logLevel = options.valueOf(logLevelOpt);
|
||||
@ -724,6 +751,8 @@ public class Config {
|
||||
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
||||
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
|
||||
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
|
||||
} catch (OptionException ex) {
|
||||
throw new ConfigException("problem parsing option '%s': %s",
|
||||
ex.options().get(0),
|
||||
@ -733,11 +762,11 @@ public class Config {
|
||||
}
|
||||
|
||||
// Create all appDataDir subdirectories and assign to their respective properties
|
||||
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
|
||||
this.keyStorageDir = mkdir(btcNetworkDir, "keys");
|
||||
this.storageDir = mkdir(btcNetworkDir, "db");
|
||||
this.torDir = mkdir(btcNetworkDir, "tor");
|
||||
this.walletDir = mkdir(btcNetworkDir, "wallet");
|
||||
File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
|
||||
this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
|
||||
this.storageDir = mkdir(xmrNetworkDir, "db");
|
||||
this.torDir = mkdir(xmrNetworkDir, "tor");
|
||||
this.walletDir = mkdir(xmrNetworkDir, "wallet");
|
||||
|
||||
// Assign values to special-case static fields
|
||||
APP_DATA_DIR_VALUE = appDataDir;
|
||||
|
@ -110,7 +110,7 @@ public final class KeyRing {
|
||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||
*/
|
||||
public void generateKeys(String password) {
|
||||
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
||||
if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys");
|
||||
symmetricKey = Encryption.generateSecretKey(256);
|
||||
signatureKeyPair = Sig.generateKeyPair();
|
||||
encryptionKeyPair = Encryption.generateKeyPair();
|
||||
|
@ -243,6 +243,11 @@ public class KeyStorage {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
storageDir.mkdirs();
|
||||
|
||||
// password must be ascii
|
||||
if (password != null && !password.matches("\\p{ASCII}*")) {
|
||||
throw new IllegalArgumentException("Password must be ASCII.");
|
||||
}
|
||||
|
||||
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
|
||||
var passwordChars = password == null ? new char[0] : password.toCharArray();
|
||||
try {
|
||||
|
@ -32,6 +32,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
@ -40,10 +41,13 @@ import java.util.Scanner;
|
||||
|
||||
@Slf4j
|
||||
public class FileUtil {
|
||||
|
||||
private static final String BACKUP_DIR = "backup";
|
||||
|
||||
public static void rollingBackup(File dir, String fileName, int numMaxBackupFiles) {
|
||||
if (numMaxBackupFiles <= 0) return;
|
||||
if (dir.exists()) {
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
|
||||
if (!backupDir.exists())
|
||||
if (!backupDir.mkdir())
|
||||
log.warn("make dir failed.\nBackupDir=" + backupDir.getAbsolutePath());
|
||||
@ -65,15 +69,32 @@ public class FileUtil {
|
||||
|
||||
pruneBackup(backupFileDir, numMaxBackupFiles);
|
||||
} catch (IOException e) {
|
||||
log.error("Backup key failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("Backup key failed: {}\n", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<File> getBackupFiles(File dir, String fileName) {
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
|
||||
if (!backupDir.exists()) return new ArrayList<File>();
|
||||
String dirName = "backups_" + fileName;
|
||||
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
|
||||
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
|
||||
if (!backupFileDir.exists()) return new ArrayList<File>();
|
||||
File[] files = backupFileDir.listFiles();
|
||||
return Arrays.asList(files);
|
||||
}
|
||||
|
||||
public static File getLatestBackupFile(File dir, String fileName) {
|
||||
List<File> files = getBackupFiles(dir, fileName);
|
||||
if (files.isEmpty()) return null;
|
||||
files.sort(Comparator.comparing(File::getName));
|
||||
return files.get(files.size() - 1);
|
||||
}
|
||||
|
||||
public static void deleteRollingBackup(File dir, String fileName) {
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
|
||||
if (!backupDir.exists()) return;
|
||||
String dirName = "backups_" + fileName;
|
||||
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
|
||||
@ -81,7 +102,7 @@ public class FileUtil {
|
||||
try {
|
||||
FileUtils.deleteDirectory(backupFileDir);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("Delete backup key failed: {}\n", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,8 +178,7 @@ public class FileUtil {
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log.error(t.toString());
|
||||
t.printStackTrace();
|
||||
log.error("Could not delete file, error={}\n", t.getMessage(), t);
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
|
@ -69,11 +69,7 @@ public class CommonSetup {
|
||||
"The system tray is not supported on the current platform.".equals(throwable.getMessage())) {
|
||||
log.warn(throwable.getMessage());
|
||||
} else {
|
||||
log.error("Uncaught Exception from thread " + Thread.currentThread().getName());
|
||||
log.error("throwableMessage= " + throwable.getMessage());
|
||||
log.error("throwableClass= " + throwable.getClass());
|
||||
log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable));
|
||||
throwable.printStackTrace();
|
||||
log.error("Uncaught Exception from thread {}, error={}\n", Thread.currentThread().getName(), throwable.getMessage(), throwable);
|
||||
UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false));
|
||||
}
|
||||
};
|
||||
@ -113,8 +109,7 @@ public class CommonSetup {
|
||||
if (!pathOfCodeSource.endsWith("classes"))
|
||||
log.info("Path to Haveno jar file: " + pathOfCodeSource);
|
||||
} catch (URISyntaxException e) {
|
||||
log.error(e.toString());
|
||||
e.printStackTrace();
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ public abstract class Task<T extends Model> {
|
||||
// t.printStackTrace(pw);
|
||||
// errorMessage = sw.toString();
|
||||
|
||||
if (taskHandler.isCanceled()) return;
|
||||
errorMessage = t.getMessage() + " (task " + getClass().getSimpleName() + ")";
|
||||
log.error(errorMessage, t);
|
||||
taskHandler.handleErrorMessage(errorMessage);
|
||||
|
@ -25,6 +25,8 @@ import java.util.Arrays;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
@Slf4j
|
||||
public class TaskRunner<T extends Model> {
|
||||
private final Queue<Class<? extends Task<T>>> tasks = new LinkedBlockingQueue<>();
|
||||
@ -67,8 +69,8 @@ public class TaskRunner<T extends Model> {
|
||||
log.info("Run task: " + currentTask.getSimpleName());
|
||||
currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run();
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
handleErrorMessage("Error at taskRunner: " + throwable.getMessage());
|
||||
log.error(ExceptionUtils.getStackTrace(throwable));
|
||||
handleErrorMessage("Error at taskRunner, error=" + throwable.getMessage());
|
||||
}
|
||||
} else {
|
||||
resultHandler.handleResult();
|
||||
@ -80,11 +82,16 @@ public class TaskRunner<T extends Model> {
|
||||
isCanceled = true;
|
||||
}
|
||||
|
||||
public boolean isCanceled() {
|
||||
return isCanceled;
|
||||
}
|
||||
|
||||
void handleComplete() {
|
||||
next();
|
||||
}
|
||||
|
||||
void handleErrorMessage(String errorMessage) {
|
||||
if (isCanceled) return;
|
||||
log.error("Task failed: " + currentTask.getSimpleName() + " / errorMessage: " + errorMessage);
|
||||
failed = true;
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
|
@ -11,8 +11,8 @@
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
* You should have received a copy of the GNU Affero General Public
|
||||
* License along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.common.util;
|
||||
@ -25,38 +25,67 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/**
|
||||
* Utility class for creating single-threaded executors.
|
||||
*/
|
||||
public class SingleThreadExecutorUtils {
|
||||
|
||||
private SingleThreadExecutorUtils() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
public static ExecutorService getSingleThreadExecutor(Class<?> aClass) {
|
||||
String name = aClass.getSimpleName();
|
||||
return getSingleThreadExecutor(name);
|
||||
validateClass(aClass);
|
||||
return getSingleThreadExecutor(aClass.getSimpleName());
|
||||
}
|
||||
|
||||
public static ExecutorService getNonDaemonSingleThreadExecutor(Class<?> aClass) {
|
||||
String name = aClass.getSimpleName();
|
||||
return getSingleThreadExecutor(name, false);
|
||||
validateClass(aClass);
|
||||
return getSingleThreadExecutor(aClass.getSimpleName(), false);
|
||||
}
|
||||
|
||||
public static ExecutorService getSingleThreadExecutor(String name) {
|
||||
validateName(name);
|
||||
return getSingleThreadExecutor(name, true);
|
||||
}
|
||||
|
||||
public static ListeningExecutorService getSingleThreadListeningExecutor(String name) {
|
||||
validateName(name);
|
||||
return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name));
|
||||
}
|
||||
|
||||
public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) {
|
||||
validateThreadFactory(threadFactory);
|
||||
return Executors.newSingleThreadExecutor(threadFactory);
|
||||
}
|
||||
|
||||
private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) {
|
||||
final ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
|
||||
ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
|
||||
return Executors.newSingleThreadExecutor(threadFactory);
|
||||
}
|
||||
|
||||
private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) {
|
||||
return new ThreadFactoryBuilder()
|
||||
.setNameFormat(name)
|
||||
.setNameFormat(name + "-%d")
|
||||
.setDaemon(isDaemonThread)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static void validateClass(Class<?> aClass) {
|
||||
if (aClass == null) {
|
||||
throw new IllegalArgumentException("Class must not be null.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateName(String name) {
|
||||
if (name == null || name.isEmpty()) {
|
||||
throw new IllegalArgumentException("Name must not be null or empty.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateThreadFactory(ThreadFactory threadFactory) {
|
||||
if (threadFactory == null) {
|
||||
throw new IllegalArgumentException("ThreadFactory must not be null.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -331,8 +331,7 @@ public class Utilities {
|
||||
clipboard.setContent(clipboardContent);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error("copyToClipboard failed " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("copyToClipboard failed: {}\n", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@ -38,13 +39,14 @@ public class ZipUtils {
|
||||
* @param dir The directory to create the zip from.
|
||||
* @param out The stream to write to.
|
||||
*/
|
||||
public static void zipDirToStream(File dir, OutputStream out, int bufferSize) throws Exception {
|
||||
public static void zipDirToStream(File dir, OutputStream out, int bufferSize, Collection<File> excludedFiles) throws Exception {
|
||||
|
||||
// Get all files in directory and subdirectories.
|
||||
ArrayList<String> fileList = new ArrayList<>();
|
||||
getFilesRecursive(dir, fileList);
|
||||
List<File> fileList = new ArrayList<>();
|
||||
getFilesRecursive(dir, fileList, excludedFiles);
|
||||
try (ZipOutputStream zos = new ZipOutputStream(out)) {
|
||||
for (String filePath : fileList) {
|
||||
for (File file : fileList) {
|
||||
String filePath = file.getAbsolutePath();
|
||||
log.info("Compressing: " + filePath);
|
||||
|
||||
// Creates a zip entry.
|
||||
@ -73,14 +75,15 @@ public class ZipUtils {
|
||||
/**
|
||||
* Get files list from the directory recursive to the subdirectory.
|
||||
*/
|
||||
public static void getFilesRecursive(File directory, List<String> fileList) {
|
||||
public static void getFilesRecursive(File directory, List<File> fileList, Collection<File> excludedFiles) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
if (excludedFiles != null && excludedFiles.contains(file)) continue;
|
||||
if (file.isFile()) {
|
||||
fileList.add(file.getAbsolutePath());
|
||||
fileList.add(file);
|
||||
} else {
|
||||
getFilesRecursive(file, fileList);
|
||||
getFilesRecursive(file, fileList, excludedFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ public class SignedWitnessService {
|
||||
} else {
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
public void onDataReceived() {
|
||||
onBootstrapComplete();
|
||||
}
|
||||
});
|
||||
@ -335,12 +335,13 @@ public class SignedWitnessService {
|
||||
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
|
||||
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
|
||||
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
|
||||
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
|
||||
String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
|
||||
if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
|
||||
key.verifyMessage(message, signatureBase64);
|
||||
verifySignatureWithECKeyResultCache.put(hash, true);
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Provided EC key is not in list of valid arbitrators.");
|
||||
log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
|
||||
verifySignatureWithECKeyResultCache.put(hash, false);
|
||||
return false;
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection;
|
||||
import haveno.core.offer.OfferRestrictions;
|
||||
import haveno.core.payment.ChargeBackRisk;
|
||||
import haveno.core.payment.PaymentAccount;
|
||||
import haveno.core.payment.TradeLimits;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
import haveno.core.payment.payload.PaymentMethod;
|
||||
import haveno.core.support.dispute.Dispute;
|
||||
@ -200,7 +201,7 @@ public class AccountAgeWitnessService {
|
||||
} else {
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
public void onDataReceived() {
|
||||
onBootStrapped();
|
||||
}
|
||||
});
|
||||
@ -498,10 +499,15 @@ public class AccountAgeWitnessService {
|
||||
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
|
||||
}
|
||||
|
||||
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) {
|
||||
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) {
|
||||
if (paymentAccount == null)
|
||||
return 0;
|
||||
|
||||
if (buyerAsTakerWithoutDeposit) {
|
||||
TradeLimits tradeLimits = new TradeLimits();
|
||||
return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
|
||||
}
|
||||
|
||||
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
|
||||
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
|
||||
if (hasTradeLimitException(accountAgeWitness)) {
|
||||
@ -737,14 +743,13 @@ public class AccountAgeWitnessService {
|
||||
}
|
||||
|
||||
public Optional<SignedWitness> traderSignAndPublishPeersAccountAgeWitness(Trade trade) {
|
||||
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
|
||||
BigInteger tradeAmount = trade.getAmount();
|
||||
checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring");
|
||||
PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey();
|
||||
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}",
|
||||
trade.toString());
|
||||
checkNotNull(tradeAmount, "Trade amount must not be null");
|
||||
checkNotNull(peersPubKey, "Peers pub key must not be null");
|
||||
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
|
||||
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString());
|
||||
BigInteger tradeAmount = trade.getAmount();
|
||||
checkNotNull(tradeAmount, "Trade amount must not be null");
|
||||
|
||||
try {
|
||||
return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey);
|
||||
|
@ -27,11 +27,13 @@ import haveno.common.crypto.KeyStorage;
|
||||
import haveno.common.file.FileUtil;
|
||||
import haveno.common.persistence.PersistenceManager;
|
||||
import haveno.common.util.ZipUtils;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
@ -139,6 +141,7 @@ public class CoreAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: share common code with BackupView to backup
|
||||
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
|
||||
if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account");
|
||||
|
||||
@ -149,9 +152,16 @@ public class CoreAccountService {
|
||||
PipedInputStream in = new PipedInputStream(bufferSize); // pipe the serialized account object to stream which will be read by the consumer
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
log.info("Zipping directory " + dataDir);
|
||||
|
||||
// exclude monero binaries from backup so they're reinstalled with permissions
|
||||
List<File> excludedFiles = Arrays.asList(
|
||||
new File(XmrWalletService.MONERO_WALLET_RPC_PATH),
|
||||
new File(XmrLocalNode.MONEROD_PATH)
|
||||
);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ZipUtils.zipDirToStream(dataDir, out, bufferSize);
|
||||
ZipUtils.zipDirToStream(dataDir, out, bufferSize, excludedFiles);
|
||||
} catch (Exception ex) {
|
||||
error.accept(ex);
|
||||
}
|
||||
|
@ -199,15 +199,15 @@ public class CoreApi {
|
||||
// Monero Connections
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addMoneroConnection(MoneroRpcConnection connection) {
|
||||
public void addXmrConnection(MoneroRpcConnection connection) {
|
||||
xmrConnectionService.addConnection(connection);
|
||||
}
|
||||
|
||||
public void removeMoneroConnection(String connectionUri) {
|
||||
public void removeXmrConnection(String connectionUri) {
|
||||
xmrConnectionService.removeConnection(connectionUri);
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getMoneroConnection() {
|
||||
public MoneroRpcConnection getXmrConnection() {
|
||||
return xmrConnectionService.getConnection();
|
||||
}
|
||||
|
||||
@ -215,15 +215,15 @@ public class CoreApi {
|
||||
return xmrConnectionService.getConnections();
|
||||
}
|
||||
|
||||
public void setMoneroConnection(String connectionUri) {
|
||||
public void setXmrConnection(String connectionUri) {
|
||||
xmrConnectionService.setConnection(connectionUri);
|
||||
}
|
||||
|
||||
public void setMoneroConnection(MoneroRpcConnection connection) {
|
||||
public void setXmrConnection(MoneroRpcConnection connection) {
|
||||
xmrConnectionService.setConnection(connection);
|
||||
}
|
||||
|
||||
public MoneroRpcConnection checkMoneroConnection() {
|
||||
public MoneroRpcConnection checkXmrConnection() {
|
||||
return xmrConnectionService.checkConnection();
|
||||
}
|
||||
|
||||
@ -231,22 +231,26 @@ public class CoreApi {
|
||||
return xmrConnectionService.checkConnections();
|
||||
}
|
||||
|
||||
public void startCheckingMoneroConnection(Long refreshPeriod) {
|
||||
public void startCheckingXmrConnection(Long refreshPeriod) {
|
||||
xmrConnectionService.startCheckingConnection(refreshPeriod);
|
||||
}
|
||||
|
||||
public void stopCheckingMoneroConnection() {
|
||||
public void stopCheckingXmrConnection() {
|
||||
xmrConnectionService.stopCheckingConnection();
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableMoneroConnection() {
|
||||
return xmrConnectionService.getBestAvailableConnection();
|
||||
public MoneroRpcConnection getBestXmrConnection() {
|
||||
return xmrConnectionService.getBestConnection();
|
||||
}
|
||||
|
||||
public void setMoneroConnectionAutoSwitch(boolean autoSwitch) {
|
||||
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
|
||||
xmrConnectionService.setAutoSwitch(autoSwitch);
|
||||
}
|
||||
|
||||
public boolean getXmrConnectionAutoSwitch() {
|
||||
return xmrConnectionService.getAutoSwitch();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Monero node
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -260,11 +264,11 @@ public class CoreApi {
|
||||
}
|
||||
|
||||
public void startXmrNode(XmrNodeSettings settings) throws IOException {
|
||||
xmrLocalNode.startNode(settings);
|
||||
xmrLocalNode.start(settings);
|
||||
}
|
||||
|
||||
public void stopXmrNode() {
|
||||
xmrLocalNode.stopNode();
|
||||
xmrLocalNode.stop();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -409,18 +413,22 @@ public class CoreApi {
|
||||
}
|
||||
|
||||
public void postOffer(String currencyCode,
|
||||
String directionAsString,
|
||||
String priceAsString,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
long amountAsLong,
|
||||
long minAmountAsLong,
|
||||
double buyerSecurityDeposit,
|
||||
String triggerPriceAsString,
|
||||
boolean reserveExactAmount,
|
||||
String paymentAccountId,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
String directionAsString,
|
||||
String priceAsString,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
long amountAsLong,
|
||||
long minAmountAsLong,
|
||||
double securityDepositPct,
|
||||
String triggerPriceAsString,
|
||||
boolean reserveExactAmount,
|
||||
String paymentAccountId,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo,
|
||||
String sourceOfferId,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
coreOffersService.postOffer(currencyCode,
|
||||
directionAsString,
|
||||
priceAsString,
|
||||
@ -428,10 +436,14 @@ public class CoreApi {
|
||||
marketPriceMargin,
|
||||
amountAsLong,
|
||||
minAmountAsLong,
|
||||
buyerSecurityDeposit,
|
||||
securityDepositPct,
|
||||
triggerPriceAsString,
|
||||
reserveExactAmount,
|
||||
paymentAccountId,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo,
|
||||
sourceOfferId,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
}
|
||||
@ -444,8 +456,11 @@ public class CoreApi {
|
||||
double marketPriceMargin,
|
||||
BigInteger amount,
|
||||
BigInteger minAmount,
|
||||
double buyerSecurityDeposit,
|
||||
PaymentAccount paymentAccount) {
|
||||
double securityDepositPct,
|
||||
PaymentAccount paymentAccount,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo) {
|
||||
return coreOffersService.editOffer(offerId,
|
||||
currencyCode,
|
||||
direction,
|
||||
@ -454,8 +469,11 @@ public class CoreApi {
|
||||
marketPriceMargin,
|
||||
amount,
|
||||
minAmount,
|
||||
buyerSecurityDeposit,
|
||||
paymentAccount);
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
}
|
||||
|
||||
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
@ -496,6 +514,10 @@ public class CoreApi {
|
||||
tradeInstant);
|
||||
}
|
||||
|
||||
public void deletePaymentAccount(String paymentAccountId) {
|
||||
paymentAccountsService.deletePaymentAccount(paymentAccountId);
|
||||
}
|
||||
|
||||
public List<PaymentMethod> getCryptoCurrencyPaymentMethods() {
|
||||
return paymentAccountsService.getCryptoCurrencyPaymentMethods();
|
||||
}
|
||||
@ -527,9 +549,11 @@ public class CoreApi {
|
||||
public void takeOffer(String offerId,
|
||||
String paymentAccountId,
|
||||
long amountAsLong,
|
||||
String challenge,
|
||||
Consumer<Trade> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
Offer offer = coreOffersService.getOffer(offerId);
|
||||
offer.setChallenge(challenge);
|
||||
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
@ -557,10 +581,6 @@ public class CoreApi {
|
||||
return coreTradesService.getTrades();
|
||||
}
|
||||
|
||||
public String getTradeRole(String tradeId) {
|
||||
return coreTradesService.getTradeRole(tradeId);
|
||||
}
|
||||
|
||||
public List<ChatMessage> getChatMessages(String tradeId) {
|
||||
return coreTradesService.getChatMessages(tradeId);
|
||||
}
|
||||
|
@ -52,6 +52,9 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
@ -59,11 +62,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class CoreDisputesService {
|
||||
|
||||
public enum DisputePayout {
|
||||
// TODO: persist in DisputeResult?
|
||||
public enum PayoutSuggestion {
|
||||
BUYER_GETS_TRADE_AMOUNT,
|
||||
BUYER_GETS_ALL, // used in desktop
|
||||
BUYER_GETS_ALL,
|
||||
SELLER_GETS_TRADE_AMOUNT,
|
||||
SELLER_GETS_ALL, // used in desktop
|
||||
SELLER_GETS_ALL,
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
@ -112,13 +116,13 @@ public class CoreDisputesService {
|
||||
|
||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||
disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler);
|
||||
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
|
||||
tradeManager.requestPersistence();
|
||||
}, trade.getId());
|
||||
}
|
||||
|
||||
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
byte[] payoutTxSerialized = null;
|
||||
String payoutTxHashAsString = null;
|
||||
|
||||
@ -163,23 +167,23 @@ public class CoreDisputesService {
|
||||
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
try {
|
||||
|
||||
// create dispute result
|
||||
var closeDate = new Date();
|
||||
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
|
||||
DisputePayout payout;
|
||||
PayoutSuggestion payoutSuggestion;
|
||||
if (customWinnerAmount > 0) {
|
||||
payout = DisputePayout.CUSTOM;
|
||||
payoutSuggestion = PayoutSuggestion.CUSTOM;
|
||||
} else if (winner == DisputeResult.Winner.BUYER) {
|
||||
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
|
||||
payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
|
||||
} else if (winner == DisputeResult.Winner.SELLER) {
|
||||
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
|
||||
payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
||||
}
|
||||
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
|
||||
applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
|
||||
|
||||
// close winning dispute ticket
|
||||
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
|
||||
@ -204,7 +208,7 @@ public class CoreDisputesService {
|
||||
throw new IllegalStateException(errMessage, err);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage());
|
||||
}
|
||||
}
|
||||
@ -224,26 +228,26 @@ public class CoreDisputesService {
|
||||
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
|
||||
* receives the remaining amount minus the mining fee.
|
||||
*/
|
||||
public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
|
||||
public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
|
||||
Contract contract = dispute.getContract();
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
|
||||
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
|
||||
BigInteger tradeAmount = contract.getTradeAmount();
|
||||
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
|
||||
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
|
||||
if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) {
|
||||
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
|
||||
} else if (payout == DisputePayout.BUYER_GETS_ALL) {
|
||||
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
|
||||
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
|
||||
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
|
||||
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
|
||||
} else if (payout == DisputePayout.SELLER_GETS_ALL) {
|
||||
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
|
||||
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
|
||||
} else if (payout == DisputePayout.CUSTOM) {
|
||||
} else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
|
||||
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
|
||||
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
|
||||
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
|
||||
@ -275,10 +279,12 @@ public class CoreDisputesService {
|
||||
disputeResult.summaryNotesProperty().get()
|
||||
);
|
||||
|
||||
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
||||
synchronized (dispute.getChatMessages()) {
|
||||
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
||||
dispute.getChatMessages().size() > 1 &&
|
||||
dispute.getChatMessages().get(1).isSystemMessage()) {
|
||||
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
|
||||
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
||||
|
@ -3,7 +3,12 @@ package haveno.core.api;
|
||||
import com.google.inject.Singleton;
|
||||
import haveno.core.api.model.TradeInfo;
|
||||
import haveno.core.support.messages.ChatMessage;
|
||||
import haveno.core.trade.BuyerTrade;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.MakerTrade;
|
||||
import haveno.core.trade.SellerTrade;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.Trade.Phase;
|
||||
import haveno.proto.grpc.NotificationMessage;
|
||||
import haveno.proto.grpc.NotificationMessage.NotificationType;
|
||||
import java.util.Iterator;
|
||||
@ -46,7 +51,18 @@ public class CoreNotificationService {
|
||||
.build());
|
||||
}
|
||||
|
||||
public void sendTradeNotification(Trade trade, String title, String message) {
|
||||
public void sendTradeNotification(Trade trade, Phase phase, String title, String message) {
|
||||
|
||||
// play chime when maker's trade is taken
|
||||
if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound();
|
||||
|
||||
// play chime when buyer can confirm payment sent
|
||||
if (trade instanceof BuyerTrade && phase == Trade.Phase.DEPOSITS_UNLOCKED) HavenoUtils.playChimeSound();
|
||||
|
||||
// play chime when seller sees buyer confirm payment sent
|
||||
if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound();
|
||||
|
||||
// send notification
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.TRADE_UPDATE)
|
||||
.setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage())
|
||||
@ -57,6 +73,7 @@ public class CoreNotificationService {
|
||||
}
|
||||
|
||||
public void sendChatNotification(ChatMessage chatMessage) {
|
||||
HavenoUtils.playChimeSound();
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.CHAT_MESSAGE)
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
|
@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply;
|
||||
import static haveno.common.util.MathUtils.roundDoubleToLong;
|
||||
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
|
||||
import haveno.core.locale.CurrencyUtil;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.monetary.CryptoMoney;
|
||||
import haveno.core.monetary.Price;
|
||||
import haveno.core.monetary.TraditionalMoney;
|
||||
@ -66,9 +67,7 @@ import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import static java.util.Comparator.comparing;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
@ -124,7 +123,6 @@ public class CoreOffersService {
|
||||
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
|
||||
return offers;
|
||||
}
|
||||
|
||||
@ -143,12 +141,9 @@ public class CoreOffersService {
|
||||
}
|
||||
|
||||
List<OpenOffer> getMyOffers() {
|
||||
List<OpenOffer> offers = openOfferManager.getOpenOffers().stream()
|
||||
return openOfferManager.getOpenOffers().stream()
|
||||
.filter(o -> o.getOffer().isMyOffer(keyRing))
|
||||
.collect(Collectors.toList());
|
||||
Set<Offer> offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images
|
||||
Set<String> offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet());
|
||||
return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList());
|
||||
};
|
||||
|
||||
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
|
||||
@ -159,7 +154,7 @@ public class CoreOffersService {
|
||||
}
|
||||
|
||||
OpenOffer getMyOffer(String id) {
|
||||
return openOfferManager.getOpenOfferById(id)
|
||||
return openOfferManager.getOpenOffer(id)
|
||||
.filter(open -> open.getOffer().isMyOffer(keyRing))
|
||||
.orElseThrow(() ->
|
||||
new IllegalStateException(format("openoffer with id '%s' not found", id)));
|
||||
@ -172,19 +167,38 @@ public class CoreOffersService {
|
||||
double marketPriceMargin,
|
||||
long amountAsLong,
|
||||
long minAmountAsLong,
|
||||
double securityDeposit,
|
||||
double securityDepositPct,
|
||||
String triggerPriceAsString,
|
||||
boolean reserveExactAmount,
|
||||
String paymentAccountId,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo,
|
||||
String sourceOfferId,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
coreWalletsService.verifyWalletsAreAvailable();
|
||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||
|
||||
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
|
||||
if (paymentAccount == null)
|
||||
throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
|
||||
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
|
||||
|
||||
// clone offer if sourceOfferId given
|
||||
if (!sourceOfferId.isEmpty()) {
|
||||
cloneOffer(sourceOfferId,
|
||||
currencyCode,
|
||||
priceAsString,
|
||||
useMarketBasedPrice,
|
||||
marketPriceMargin,
|
||||
triggerPriceAsString,
|
||||
paymentAccountId,
|
||||
extraInfo,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// create new offer
|
||||
String upperCaseCurrencyCode = currencyCode.toUpperCase();
|
||||
String offerId = createOfferService.getRandomOfferId();
|
||||
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
|
||||
@ -199,22 +213,78 @@ public class CoreOffersService {
|
||||
price,
|
||||
useMarketBasedPrice,
|
||||
exactMultiply(marketPriceMargin, 0.01),
|
||||
securityDeposit,
|
||||
paymentAccount);
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
|
||||
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
||||
|
||||
// We don't support atm funding from external wallet to keep it simple.
|
||||
boolean useSavingsWallet = true;
|
||||
//noinspection ConstantConditions
|
||||
placeOffer(offer,
|
||||
triggerPriceAsString,
|
||||
useSavingsWallet,
|
||||
true,
|
||||
reserveExactAmount,
|
||||
null,
|
||||
transaction -> resultHandler.accept(offer),
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
||||
private void cloneOffer(String sourceOfferId,
|
||||
String currencyCode,
|
||||
String priceAsString,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
String triggerPriceAsString,
|
||||
String paymentAccountId,
|
||||
String extraInfo,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
|
||||
// get source offer
|
||||
OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId);
|
||||
Offer sourceOffer = sourceOpenOffer.getOffer();
|
||||
|
||||
// get trade currency (default source currency)
|
||||
if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode();
|
||||
if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode();
|
||||
String upperCaseCurrencyCode = currencyCode.toUpperCase();
|
||||
|
||||
// get price (default source price)
|
||||
Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString);
|
||||
if (price == null) useMarketBasedPrice = true;
|
||||
|
||||
// get payment account
|
||||
if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
|
||||
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
|
||||
if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
|
||||
|
||||
// get extra info
|
||||
if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
|
||||
|
||||
// create cloned offer
|
||||
Offer offer = createOfferService.createClonedOffer(sourceOffer,
|
||||
upperCaseCurrencyCode,
|
||||
price,
|
||||
useMarketBasedPrice,
|
||||
exactMultiply(marketPriceMargin, 0.01),
|
||||
paymentAccount,
|
||||
extraInfo);
|
||||
|
||||
// verify cloned offer
|
||||
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
||||
|
||||
// place offer
|
||||
placeOffer(offer,
|
||||
triggerPriceAsString,
|
||||
true,
|
||||
false, // ignored when cloning
|
||||
sourceOfferId,
|
||||
transaction -> resultHandler.accept(offer),
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
||||
// TODO: this implementation is missing; implement.
|
||||
Offer editOffer(String offerId,
|
||||
String currencyCode,
|
||||
OfferDirection direction,
|
||||
@ -223,8 +293,11 @@ public class CoreOffersService {
|
||||
double marketPriceMargin,
|
||||
BigInteger amount,
|
||||
BigInteger minAmount,
|
||||
double buyerSecurityDeposit,
|
||||
PaymentAccount paymentAccount) {
|
||||
double securityDepositPct,
|
||||
PaymentAccount paymentAccount,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo) {
|
||||
return createOfferService.createAndGetOffer(offerId,
|
||||
direction,
|
||||
currencyCode.toUpperCase(),
|
||||
@ -233,8 +306,11 @@ public class CoreOffersService {
|
||||
price,
|
||||
useMarketBasedPrice,
|
||||
exactMultiply(marketPriceMargin, 0.01),
|
||||
buyerSecurityDeposit,
|
||||
paymentAccount);
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
}
|
||||
|
||||
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
@ -244,26 +320,6 @@ public class CoreOffersService {
|
||||
|
||||
// -------------------------- PRIVATE HELPERS -----------------------------
|
||||
|
||||
private Set<Offer> getOffersWithDuplicateKeyImages(List<Offer> offers) {
|
||||
Set<Offer> duplicateFundedOffers = new HashSet<Offer>();
|
||||
Set<String> seenKeyImages = new HashSet<String>();
|
||||
for (Offer offer : offers) {
|
||||
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
|
||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||
if (!seenKeyImages.add(keyImage)) {
|
||||
for (Offer offer2 : offers) {
|
||||
if (offer == offer2) continue;
|
||||
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
||||
log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
|
||||
duplicateFundedOffers.add(offer2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return duplicateFundedOffers;
|
||||
}
|
||||
|
||||
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
|
||||
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
|
||||
String error = format("cannot create %s offer with payment account %s",
|
||||
@ -277,6 +333,7 @@ public class CoreOffersService {
|
||||
String triggerPriceAsString,
|
||||
boolean useSavingsWallet,
|
||||
boolean reserveExactAmount,
|
||||
String sourceOfferId,
|
||||
Consumer<Transaction> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
|
||||
@ -284,6 +341,8 @@ public class CoreOffersService {
|
||||
useSavingsWallet,
|
||||
triggerPriceAsLong,
|
||||
reserveExactAmount,
|
||||
true,
|
||||
sourceOfferId,
|
||||
resultHandler::accept,
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
@ -64,9 +64,14 @@ class CorePaymentAccountsService {
|
||||
}
|
||||
|
||||
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
|
||||
validateFormFields(form);
|
||||
PaymentAccount paymentAccount = form.toPaymentAccount();
|
||||
setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload
|
||||
verifyPaymentAccountHasRequiredFields(paymentAccount);
|
||||
if (paymentAccount instanceof CryptoCurrencyAccount) {
|
||||
CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount;
|
||||
verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress());
|
||||
}
|
||||
user.addPaymentAccountIfNotExists(paymentAccount);
|
||||
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
|
||||
log.info("Saved payment account with id {} and payment method {}.",
|
||||
@ -145,6 +150,16 @@ class CorePaymentAccountsService {
|
||||
return cryptoCurrencyAccount;
|
||||
}
|
||||
|
||||
synchronized void deletePaymentAccount(String paymentAccountId) {
|
||||
accountService.checkAccountOpen();
|
||||
PaymentAccount paymentAccount = getPaymentAccount(paymentAccountId);
|
||||
if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId));
|
||||
user.removePaymentAccount(paymentAccount);
|
||||
log.info("Deleted payment account with id {} and payment method {}.",
|
||||
paymentAccount.getId(),
|
||||
paymentAccount.getPaymentAccountPayload().getPaymentMethodId());
|
||||
}
|
||||
|
||||
// TODO Support all alt coin payment methods supported by UI.
|
||||
// The getCryptoCurrencyPaymentMethods method below will be
|
||||
// callable from the CLI when more are supported.
|
||||
@ -156,6 +171,12 @@ class CorePaymentAccountsService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void validateFormFields(PaymentAccountForm form) {
|
||||
for (PaymentAccountFormField field : form.getFields()) {
|
||||
validateFormField(form, field.getId(), field.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
|
||||
|
||||
// get payment method id
|
||||
|
@ -72,7 +72,7 @@ class CorePriceService {
|
||||
* @return Price per 1 XMR in the given currency (traditional or crypto)
|
||||
*/
|
||||
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
|
||||
var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
|
||||
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
|
||||
if (marketPrice == null) {
|
||||
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage;
|
||||
import haveno.core.support.traderchat.TradeChatSession;
|
||||
import haveno.core.support.traderchat.TraderChatManager;
|
||||
import haveno.core.trade.ClosedTradableManager;
|
||||
import haveno.core.trade.Tradable;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.TradeUtil;
|
||||
@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol;
|
||||
import haveno.core.trade.protocol.SellerProtocol;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.coin.CoinUtil;
|
||||
import haveno.core.util.validation.BtcAddressValidator;
|
||||
import haveno.core.xmr.model.AddressEntry;
|
||||
import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT;
|
||||
import haveno.core.xmr.wallet.BtcWalletService;
|
||||
import static java.lang.String.format;
|
||||
import java.math.BigInteger;
|
||||
@ -66,7 +62,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
@ -82,7 +79,6 @@ class CoreTradesService {
|
||||
private final TakeOfferModel takeOfferModel;
|
||||
private final TradeManager tradeManager;
|
||||
private final TraderChatManager traderChatManager;
|
||||
private final TradeUtil tradeUtil;
|
||||
private final OfferUtil offerUtil;
|
||||
private final User user;
|
||||
|
||||
@ -104,7 +100,6 @@ class CoreTradesService {
|
||||
this.takeOfferModel = takeOfferModel;
|
||||
this.tradeManager = tradeManager;
|
||||
this.traderChatManager = traderChatManager;
|
||||
this.tradeUtil = tradeUtil;
|
||||
this.offerUtil = offerUtil;
|
||||
this.user = user;
|
||||
}
|
||||
@ -130,7 +125,7 @@ class CoreTradesService {
|
||||
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
|
||||
String currencyCode = offer.getCurrencyCode();
|
||||
OfferDirection direction = offer.getOfferPayload().getDirection();
|
||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
|
||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
|
||||
if (offer.getPrice() != null) {
|
||||
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
|
||||
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
|
||||
@ -161,7 +156,7 @@ class CoreTradesService {
|
||||
errorMessageHandler
|
||||
);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
errorMessageHandler.handleErrorMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
@ -204,7 +199,7 @@ class CoreTradesService {
|
||||
String getTradeRole(String tradeId) {
|
||||
coreWalletsService.verifyWalletsAreAvailable();
|
||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||
return tradeUtil.getRole(getTrade(tradeId));
|
||||
return TradeUtil.getRole(getTrade(tradeId));
|
||||
}
|
||||
|
||||
Trade getTrade(String tradeId) {
|
||||
@ -221,8 +216,7 @@ class CoreTradesService {
|
||||
}
|
||||
|
||||
private Optional<Trade> getClosedTrade(String tradeId) {
|
||||
Optional<Tradable> tradable = closedTradableManager.getTradeById(tradeId);
|
||||
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
|
||||
return closedTradableManager.getTradeById(tradeId);
|
||||
}
|
||||
|
||||
List<Trade> getTrades() {
|
||||
@ -265,40 +259,9 @@ class CoreTradesService {
|
||||
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
|
||||
}
|
||||
|
||||
private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
|
||||
// TODO This and identical logic should be refactored into TradeUtil.
|
||||
try {
|
||||
return btcWalletService.getFeeEstimationTransaction(fromAddress,
|
||||
toAddress,
|
||||
amount,
|
||||
TRADE_PAYOUT).getFee();
|
||||
} catch (Exception ex) {
|
||||
log.error("", ex);
|
||||
throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// Throws a RuntimeException trade is already closed.
|
||||
private void verifyTradeIsNotClosed(String tradeId) {
|
||||
if (getClosedTrade(tradeId).isPresent())
|
||||
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
|
||||
}
|
||||
|
||||
// Throws a RuntimeException if address is not valid.
|
||||
private void verifyIsValidBTCAddress(String address) {
|
||||
try {
|
||||
new BtcAddressValidator().validate(address);
|
||||
} catch (Throwable t) {
|
||||
log.error("", t);
|
||||
throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
|
||||
}
|
||||
}
|
||||
|
||||
// Throws a RuntimeException if address has a zero balance.
|
||||
private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
|
||||
Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
|
||||
if (fromAddressBalance.isZero())
|
||||
throw new IllegalStateException(format("funds already withdrawn from address '%s'",
|
||||
fromAddressEntry.getAddressString()));
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ class CoreWalletsService {
|
||||
|
||||
List<MoneroTxWallet> getXmrTxs() {
|
||||
accountService.checkAccountOpen();
|
||||
return xmrWalletService.getWallet().getTxs();
|
||||
return xmrWalletService.getTxs();
|
||||
}
|
||||
|
||||
MoneroTxWallet createXmrTx(List<MoneroDestination> destinations) {
|
||||
@ -178,7 +178,7 @@ class CoreWalletsService {
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
try {
|
||||
return xmrWalletService.getWallet().relayTx(metadata);
|
||||
return xmrWalletService.relayTx(metadata);
|
||||
} catch (Exception ex) {
|
||||
log.error("", ex);
|
||||
throw new IllegalStateException(ex);
|
||||
|
@ -32,12 +32,18 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
|
||||
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
|
||||
import haveno.core.xmr.setup.DownloadListener;
|
||||
import haveno.core.xmr.setup.WalletsSetup;
|
||||
import haveno.core.xmr.wallet.XmrKeyImagePoller;
|
||||
import haveno.network.Socks5ProxyProvider;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import haveno.network.p2p.P2PServiceListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@ -48,6 +54,8 @@ import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroConnectionManager;
|
||||
@ -56,7 +64,6 @@ import monero.common.MoneroRpcConnection;
|
||||
import monero.common.TaskLooper;
|
||||
import monero.daemon.MoneroDaemonRpc;
|
||||
import monero.daemon.model.MoneroDaemonInfo;
|
||||
import monero.daemon.model.MoneroPeer;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
@ -65,8 +72,14 @@ public final class XmrConnectionService {
|
||||
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
||||
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
|
||||
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
||||
private static final long MIN_ERROR_LOG_PERIOD_MS = 300000; // minimum period between logging errors fetching daemon info
|
||||
private static Long lastErrorTimestamp;
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
||||
|
||||
public enum XmrConnectionFallbackType {
|
||||
LOCAL,
|
||||
CUSTOM,
|
||||
PROVIDED
|
||||
}
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final Object pollLock = new Object();
|
||||
@ -79,23 +92,45 @@ public final class XmrConnectionService {
|
||||
private final XmrLocalNode xmrLocalNode;
|
||||
private final MoneroConnectionManager connectionManager;
|
||||
private final EncryptedConnectionList connectionList;
|
||||
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
|
||||
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
|
||||
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
||||
private final DownloadListener downloadListener = new DownloadListener();
|
||||
@Getter
|
||||
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
|
||||
@Getter
|
||||
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
|
||||
private final LongProperty numUpdates = new SimpleLongProperty(0);
|
||||
private Socks5ProxyProvider socks5ProxyProvider;
|
||||
|
||||
private boolean isInitialized;
|
||||
private boolean pollInProgress;
|
||||
private MoneroDaemonRpc daemon;
|
||||
private Boolean isConnected = false;
|
||||
@Getter
|
||||
private MoneroDaemonInfo lastInfo;
|
||||
private Long syncStartHeight = null;
|
||||
private Long lastFallbackInvocation;
|
||||
private Long lastLogPollErrorTimestamp;
|
||||
private long lastLogDaemonNotSyncedTimestamp;
|
||||
private Long syncStartHeight;
|
||||
private TaskLooper daemonPollLooper;
|
||||
private long lastRefreshPeriodMs;
|
||||
@Getter
|
||||
private boolean isShutDownStarted;
|
||||
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
||||
private XmrKeyImagePoller keyImagePoller;
|
||||
|
||||
// connection switching
|
||||
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
|
||||
private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 2;
|
||||
private static final int SKIP_SWITCH_WITHIN_MS = 10000;
|
||||
private int numRequestsLastMinute;
|
||||
private long lastSwitchTimestamp;
|
||||
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
|
||||
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
|
||||
private boolean fallbackApplied;
|
||||
private boolean usedSyncingLocalNodeBeforeStartup;
|
||||
|
||||
@Inject
|
||||
public XmrConnectionService(P2PService p2PService,
|
||||
@ -123,7 +158,13 @@ public final class XmrConnectionService {
|
||||
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
initialize();
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
try {
|
||||
initialize();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void onHiddenServicePublished() {}
|
||||
@ -148,7 +189,6 @@ public final class XmrConnectionService {
|
||||
isInitialized = false;
|
||||
synchronized (lock) {
|
||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||
connectionManager.stopPolling();
|
||||
daemon = null;
|
||||
}
|
||||
}
|
||||
@ -171,7 +211,7 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
public Boolean isConnected() {
|
||||
return connectionManager.isConnected();
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public void addConnection(MoneroRpcConnection connection) {
|
||||
@ -226,13 +266,112 @@ public final class XmrConnectionService {
|
||||
|
||||
public void stopCheckingConnection() {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.stopPolling();
|
||||
connectionList.setRefreshPeriod(-1L);
|
||||
updatePolling();
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableConnection() {
|
||||
public MoneroRpcConnection getBestConnection() {
|
||||
return getBestConnection(new ArrayList<MoneroRpcConnection>());
|
||||
}
|
||||
|
||||
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
accountService.checkAccountOpen();
|
||||
return connectionManager.getBestAvailableConnection();
|
||||
|
||||
// user needs to authorize fallback on startup after using locally synced node
|
||||
if (fallbackRequiredBeforeConnectionSwitch()) {
|
||||
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
|
||||
return null;
|
||||
}
|
||||
|
||||
// get best connection
|
||||
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
|
||||
addLocalNodeIfIgnored(ignoredConnectionsSet);
|
||||
MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections
|
||||
if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0);
|
||||
return bestConnection;
|
||||
}
|
||||
|
||||
private boolean fallbackRequiredBeforeConnectionSwitch() {
|
||||
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
|
||||
}
|
||||
|
||||
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
|
||||
}
|
||||
|
||||
private void switchToBestConnection() {
|
||||
if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
|
||||
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
|
||||
return;
|
||||
}
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection);
|
||||
}
|
||||
|
||||
public synchronized boolean requestSwitchToNextBestConnection() {
|
||||
return requestSwitchToNextBestConnection(null);
|
||||
}
|
||||
|
||||
public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
log.warn("Requesting switch to next best monerod, source monerod={}", sourceConnection == null ? getConnection() == null ? null : getConnection().getUri() : sourceConnection.getUri());
|
||||
|
||||
// skip if shut down started
|
||||
if (isShutDownStarted) {
|
||||
log.warn("Skipping switch to next best Monero connection because shut down has started");
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip if connection is already switched
|
||||
if (sourceConnection != null && sourceConnection != getConnection()) {
|
||||
log.warn("Skipping switch to next best Monero connection because source connection is not current connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip if connection is fixed
|
||||
if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
|
||||
log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip if last switch was too recent
|
||||
boolean skipSwitch = System.currentTimeMillis() - lastSwitchTimestamp < SKIP_SWITCH_WITHIN_MS;
|
||||
if (skipSwitch) {
|
||||
log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip if too many requests in the last minute
|
||||
if (numRequestsLastMinute > MAX_SWITCH_REQUESTS_PER_MINUTE) {
|
||||
log.warn("Skipping switch to next best Monero connection because more than {} requests were made in the last minute", MAX_SWITCH_REQUESTS_PER_MINUTE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// increment request count
|
||||
numRequestsLastMinute++;
|
||||
UserThread.runAfter(() -> numRequestsLastMinute--, 60); // decrement after one minute
|
||||
|
||||
// exclude current connection
|
||||
MoneroRpcConnection currentConnection = getConnection();
|
||||
if (currentConnection != null) excludedConnections.add(currentConnection);
|
||||
|
||||
// get connection to switch to
|
||||
MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
|
||||
|
||||
// remove from excluded connections after period
|
||||
UserThread.runAfter(() -> {
|
||||
if (currentConnection != null) excludedConnections.remove(currentConnection);
|
||||
}, EXCLUDE_CONNECTION_SECONDS);
|
||||
|
||||
// return if no connection to switch to
|
||||
if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
|
||||
log.warn("No connection to switch to");
|
||||
return false;
|
||||
}
|
||||
|
||||
// switch to best connection
|
||||
lastSwitchTimestamp = System.currentTimeMillis();
|
||||
setConnection(bestConnection);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setAutoSwitch(boolean autoSwitch) {
|
||||
@ -241,16 +380,25 @@ public final class XmrConnectionService {
|
||||
connectionList.setAutoSwitch(autoSwitch);
|
||||
}
|
||||
|
||||
public boolean isConnectionLocal() {
|
||||
return isConnectionLocal(getConnection());
|
||||
public boolean getAutoSwitch() {
|
||||
accountService.checkAccountOpen();
|
||||
return connectionList.getAutoSwitch();
|
||||
}
|
||||
|
||||
public boolean isConnectionTor() {
|
||||
return useTorProxy(getConnection());
|
||||
public boolean isConnectionLocalHost() {
|
||||
return isConnectionLocalHost(getConnection());
|
||||
}
|
||||
|
||||
public boolean isProxyApplied() {
|
||||
return isProxyApplied(getConnection());
|
||||
}
|
||||
|
||||
public long getRefreshPeriodMs() {
|
||||
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs();
|
||||
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(false);
|
||||
}
|
||||
|
||||
private long getInternalRefreshPeriodMs() {
|
||||
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(true);
|
||||
}
|
||||
|
||||
public void verifyConnection() {
|
||||
@ -263,23 +411,33 @@ public final class XmrConnectionService {
|
||||
Long targetHeight = getTargetHeight();
|
||||
if (targetHeight == null) return false;
|
||||
if (targetHeight - chainHeight.get() <= 3) return true; // synced if within 3 blocks of target height
|
||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), targetHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
public Long getTargetHeight() {
|
||||
if (daemon == null || lastInfo == null) return null;
|
||||
if (lastInfo == null) return null;
|
||||
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
|
||||
}
|
||||
|
||||
public XmrKeyImagePoller getKeyImagePoller() {
|
||||
synchronized (lock) {
|
||||
if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
|
||||
return keyImagePoller;
|
||||
}
|
||||
}
|
||||
|
||||
private long getKeyImageRefreshPeriodMs() {
|
||||
return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
|
||||
}
|
||||
|
||||
// ----------------------------- APP METHODS ------------------------------
|
||||
|
||||
public ReadOnlyIntegerProperty numPeersProperty() {
|
||||
return numPeers;
|
||||
public ReadOnlyIntegerProperty numConnectionsProperty() {
|
||||
return numConnections;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
|
||||
return peers;
|
||||
public ReadOnlyObjectProperty<List<MoneroRpcConnection>> connectionsProperty() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() {
|
||||
@ -287,7 +445,7 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
public boolean hasSufficientPeersForBroadcast() {
|
||||
return numPeers.get() >= getMinBroadcastConnections();
|
||||
return numConnections.get() >= getMinBroadcastConnections();
|
||||
}
|
||||
|
||||
public LongProperty chainHeightProperty() {
|
||||
@ -310,36 +468,65 @@ public final class XmrConnectionService {
|
||||
return numUpdates;
|
||||
}
|
||||
|
||||
public void fallbackToBestConnection() {
|
||||
if (isShutDownStarted) return;
|
||||
fallbackApplied = true;
|
||||
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||
log.warn("Falling back to public nodes");
|
||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
||||
initializeConnections();
|
||||
} else {
|
||||
log.warn("Falling back to provided nodes");
|
||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
|
||||
initializeConnections();
|
||||
if (getConnection() == null) {
|
||||
log.warn("No provided nodes available, falling back to public nodes");
|
||||
fallbackToBestConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------- HELPERS --------------------------------
|
||||
|
||||
private void doneDownload() {
|
||||
downloadListener.doneDownload();
|
||||
}
|
||||
|
||||
private boolean isConnectionLocal(MoneroRpcConnection connection) {
|
||||
private boolean isConnectionLocalHost(MoneroRpcConnection connection) {
|
||||
return connection != null && HavenoUtils.isLocalHost(connection.getUri());
|
||||
}
|
||||
|
||||
private long getDefaultRefreshPeriodMs() {
|
||||
private long getDefaultRefreshPeriodMs(boolean internal) {
|
||||
MoneroRpcConnection connection = getConnection();
|
||||
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||
if (isConnectionLocal(connection)) {
|
||||
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
||||
else return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||
} else if (useTorProxy(connection)) {
|
||||
if (isConnectionLocalHost(connection)) {
|
||||
if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||
if (lastInfo != null && (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight())) {
|
||||
return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
||||
} else {
|
||||
return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||
}
|
||||
} else if (isProxyApplied(connection)) {
|
||||
return REFRESH_PERIOD_ONION_MS;
|
||||
} else {
|
||||
return REFRESH_PERIOD_HTTP_MS;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean useTorProxy(MoneroRpcConnection connection) {
|
||||
private boolean isProxyApplied(MoneroRpcConnection connection) {
|
||||
if (connection == null) return false;
|
||||
return connection.isOnion() || (preferences.getUseTorForXmr().isUseTorForXmr() && !HavenoUtils.isLocalHost(connection.getUri()));
|
||||
return connection.isOnion() || (preferences.getUseTorForXmr().isUseTorForXmr() && !HavenoUtils.isPrivateIp(connection.getUri()));
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
|
||||
// initialize key image poller
|
||||
getKeyImagePoller();
|
||||
new Thread(() -> {
|
||||
HavenoUtils.waitFor(20000);
|
||||
keyImagePoller.poll(); // TODO: keep or remove first poll?s
|
||||
}).start();
|
||||
|
||||
// initialize connections
|
||||
initializeConnections();
|
||||
|
||||
@ -352,7 +539,7 @@ public final class XmrConnectionService {
|
||||
log.info(getClass() + ".onAccountOpened() called");
|
||||
initialize();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("Error initializing connection service after account opened, error={}\n", e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@ -379,7 +566,7 @@ public final class XmrConnectionService {
|
||||
xmrLocalNode.addListener(new XmrLocalNodeListener() {
|
||||
@Override
|
||||
public void onNodeStarted(MoneroDaemonRpc daemon) {
|
||||
log.info("Local monero node started");
|
||||
log.info("Local monero node started, height={}", daemon.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -406,8 +593,13 @@ public final class XmrConnectionService {
|
||||
// update connection
|
||||
if (isConnected) {
|
||||
setConnection(connection.getUri());
|
||||
|
||||
// reset error connecting to local node
|
||||
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
|
||||
connectionServiceFallbackType.set(null);
|
||||
}
|
||||
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
|
||||
}
|
||||
}
|
||||
@ -415,7 +607,7 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
// restore connections
|
||||
if ("".equals(config.xmrNode)) {
|
||||
if (!isFixedConnection()) {
|
||||
|
||||
// load previous or default connections
|
||||
if (coreContext.isApiUser()) {
|
||||
@ -427,8 +619,10 @@ public final class XmrConnectionService {
|
||||
// add default connections
|
||||
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
|
||||
if (node.hasClearNetAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
||||
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
||||
}
|
||||
}
|
||||
if (node.hasOnionAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
@ -440,8 +634,10 @@ public final class XmrConnectionService {
|
||||
// add default connections
|
||||
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||
if (node.hasClearNetAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
addConnection(connection);
|
||||
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
addConnection(connection);
|
||||
}
|
||||
}
|
||||
if (node.hasOnionAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
@ -452,89 +648,75 @@ public final class XmrConnectionService {
|
||||
|
||||
// restore last connection
|
||||
if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
|
||||
connectionManager.setConnection(connectionList.getCurrentConnectionUri().get());
|
||||
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) {
|
||||
connectionManager.setConnection(connectionList.getCurrentConnectionUri().get());
|
||||
}
|
||||
}
|
||||
|
||||
// set if last node was locally syncing
|
||||
if (!isInitialized) {
|
||||
usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain();
|
||||
}
|
||||
|
||||
// set connection proxies
|
||||
log.info("TOR proxy URI: " + getProxyUri());
|
||||
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
||||
if (useTorProxy(connection)) connection.setProxyUri(getProxyUri());
|
||||
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
|
||||
}
|
||||
|
||||
// restore auto switch
|
||||
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
|
||||
else connectionManager.setAutoSwitch(true);
|
||||
|
||||
// start local node if applicable
|
||||
maybeStartLocalNode();
|
||||
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
|
||||
|
||||
// update connection
|
||||
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection);
|
||||
} else {
|
||||
checkConnection();
|
||||
}
|
||||
} else if (!isInitialized) {
|
||||
|
||||
// set connection from startup argument if given
|
||||
connectionManager.setAutoSwitch(false);
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
|
||||
if (useTorProxy(connection)) connection.setProxyUri(getProxyUri());
|
||||
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
|
||||
connectionManager.setConnection(connection);
|
||||
|
||||
// start local node if applicable
|
||||
maybeStartLocalNode();
|
||||
|
||||
// update connection
|
||||
checkConnection();
|
||||
}
|
||||
|
||||
// register connection listener
|
||||
connectionManager.addListener(this::onConnectionChanged);
|
||||
|
||||
// start polling after delay
|
||||
UserThread.runAfter(() -> {
|
||||
if (!isShutDownStarted) connectionManager.startPolling(getRefreshPeriodMs() * 2);
|
||||
}, getDefaultRefreshPeriodMs() * 2 / 1000);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
// notify initial connection
|
||||
lastRefreshPeriodMs = getRefreshPeriodMs();
|
||||
onConnectionChanged(connectionManager.getConnection());
|
||||
}
|
||||
|
||||
private void maybeStartLocalNode() {
|
||||
|
||||
// skip if seed node
|
||||
if (HavenoUtils.isSeedNode()) return;
|
||||
|
||||
// start local node if offline and used as last connection
|
||||
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
|
||||
try {
|
||||
log.info("Starting local node");
|
||||
xmrLocalNode.startMoneroNode();
|
||||
} catch (Exception e) {
|
||||
log.warn("Unable to start local monero node: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
public void startLocalNode() throws Exception {
|
||||
|
||||
// cannot start local node as seed node
|
||||
if (HavenoUtils.isSeedNode()) {
|
||||
throw new RuntimeException("Cannot start local node on seed node");
|
||||
}
|
||||
|
||||
// start local node
|
||||
log.info("Starting local node");
|
||||
xmrLocalNode.start();
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||
if (isShutDownStarted) return;
|
||||
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected());
|
||||
if (isShutDownStarted || !accountService.isAccountOpen()) return;
|
||||
if (currentConnection == null) {
|
||||
log.warn("Setting daemon connection to null");
|
||||
Thread.dumpStack();
|
||||
log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
|
||||
}
|
||||
synchronized (lock) {
|
||||
if (currentConnection == null) {
|
||||
daemon = null;
|
||||
isConnected = false;
|
||||
connectionList.setCurrentConnectionUri(null);
|
||||
} else {
|
||||
daemon = new MoneroDaemonRpc(currentConnection);
|
||||
isConnected = currentConnection.isConnected();
|
||||
connectionList.removeConnection(currentConnection.getUri());
|
||||
connectionList.addConnection(currentConnection);
|
||||
connectionList.setCurrentConnectionUri(currentConnection.getUri());
|
||||
@ -546,9 +728,18 @@ public final class XmrConnectionService {
|
||||
numUpdates.set(numUpdates.get() + 1);
|
||||
});
|
||||
}
|
||||
updatePolling();
|
||||
|
||||
// update key image poller
|
||||
keyImagePoller.setDaemon(getDaemon());
|
||||
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
||||
|
||||
// update polling
|
||||
doPollDaemon();
|
||||
if (currentConnection != getConnection()) return; // polling can change connection
|
||||
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
|
||||
|
||||
// notify listeners in parallel
|
||||
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
|
||||
synchronized (listenerLock) {
|
||||
for (MoneroConnectionManagerListener listener : listeners) {
|
||||
ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection));
|
||||
@ -557,19 +748,15 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
private void updatePolling() {
|
||||
new Thread(() -> {
|
||||
synchronized (lock) {
|
||||
stopPolling();
|
||||
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
|
||||
}
|
||||
}).start();
|
||||
stopPolling();
|
||||
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
|
||||
}
|
||||
|
||||
private void startPolling() {
|
||||
synchronized (lock) {
|
||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||
daemonPollLooper = new TaskLooper(() -> pollDaemonInfo());
|
||||
daemonPollLooper.start(getRefreshPeriodMs());
|
||||
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
||||
daemonPollLooper.start(getInternalRefreshPeriodMs());
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,17 +769,81 @@ public final class XmrConnectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private void pollDaemonInfo() {
|
||||
private void pollDaemon() {
|
||||
if (pollInProgress) return;
|
||||
doPollDaemon();
|
||||
}
|
||||
|
||||
private void doPollDaemon() {
|
||||
synchronized (pollLock) {
|
||||
pollInProgress = true;
|
||||
if (isShutDownStarted) return;
|
||||
try {
|
||||
|
||||
// poll daemon
|
||||
log.debug("Polling daemon info");
|
||||
if (daemon == null) throw new RuntimeException("No daemon connection");
|
||||
lastInfo = daemon.getInfo();
|
||||
if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
|
||||
try {
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||
lastInfo = daemon.getInfo();
|
||||
} catch (Exception e) {
|
||||
|
||||
// skip handling if shutting down
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// invoke fallback handling on startup error
|
||||
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
|
||||
if (lastInfo == null && canFallback) {
|
||||
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
|
||||
lastFallbackInvocation = System.currentTimeMillis();
|
||||
if (usedSyncingLocalNodeBeforeStartup) {
|
||||
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
|
||||
connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
|
||||
} else if (isProvidedConnections()) {
|
||||
log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage());
|
||||
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
|
||||
} else {
|
||||
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
|
||||
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// log error message periodically
|
||||
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
|
||||
log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage());
|
||||
if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e));
|
||||
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// switch to best connection
|
||||
switchToBestConnection();
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
|
||||
lastInfo = daemon.getInfo(); // caught internally if still fails
|
||||
}
|
||||
|
||||
// connected to daemon
|
||||
isConnected = true;
|
||||
connectionServiceFallbackType.set(null);
|
||||
|
||||
// determine if blockchain is syncing locally
|
||||
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
|
||||
|
||||
// write sync status to preferences
|
||||
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
|
||||
|
||||
// throttle warnings if daemon not synced
|
||||
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) {
|
||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight());
|
||||
lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// announce connection change if refresh period changes
|
||||
if (getRefreshPeriodMs() != lastRefreshPeriodMs) {
|
||||
lastRefreshPeriodMs = getRefreshPeriodMs();
|
||||
onConnectionChanged(getConnection()); // causes new poll
|
||||
return;
|
||||
}
|
||||
|
||||
// update properties on user thread
|
||||
UserThread.execute(() -> {
|
||||
@ -607,79 +858,57 @@ public final class XmrConnectionService {
|
||||
long targetHeight = lastInfo.getTargetHeight();
|
||||
long blocksLeft = targetHeight - lastInfo.getHeight();
|
||||
if (syncStartHeight == null) syncStartHeight = lastInfo.getHeight();
|
||||
double percent = targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight)) * 100d; // grant at least 1 block to show progress
|
||||
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress
|
||||
downloadListener.progress(percent, blocksLeft, null);
|
||||
}
|
||||
|
||||
// set peer connections
|
||||
// TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections
|
||||
// try {
|
||||
// peers.set(getOnlinePeers());
|
||||
// } catch (Exception err) {
|
||||
// // TODO: peers unknown due to restricted RPC call
|
||||
// }
|
||||
// numPeers.set(peers.get().size());
|
||||
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
|
||||
peers.set(new ArrayList<MoneroPeer>());
|
||||
// set available connections
|
||||
List<MoneroRpcConnection> availableConnections = new ArrayList<>();
|
||||
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
||||
if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) {
|
||||
availableConnections.add(connection);
|
||||
}
|
||||
}
|
||||
connections.set(availableConnections);
|
||||
numConnections.set(availableConnections.size());
|
||||
|
||||
// notify update
|
||||
numUpdates.set(numUpdates.get() + 1);
|
||||
});
|
||||
|
||||
// handle error recovery
|
||||
if (lastErrorTimestamp != null) {
|
||||
if (lastLogPollErrorTimestamp != null) {
|
||||
log.info("Successfully fetched daemon info after previous error");
|
||||
lastErrorTimestamp = null;
|
||||
}
|
||||
|
||||
// update and notify connected state
|
||||
if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
|
||||
connectionManager.checkConnection();
|
||||
lastLogPollErrorTimestamp = null;
|
||||
}
|
||||
|
||||
// clear error message
|
||||
if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
|
||||
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
|
||||
}
|
||||
getConnectionServiceErrorMsg().set(null);
|
||||
} catch (Exception e) {
|
||||
|
||||
// skip if shut down or connected
|
||||
if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return;
|
||||
// not connected to daemon
|
||||
isConnected = false;
|
||||
|
||||
// log error message periodically
|
||||
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
|
||||
lastErrorTimestamp = System.currentTimeMillis();
|
||||
log.warn("Could not update daemon info: " + e.getMessage());
|
||||
if (DevEnv.isDevMode()) e.printStackTrace();
|
||||
}
|
||||
// skip if shut down
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
new Thread(() -> {
|
||||
if (isShutDownStarted) return;
|
||||
if (!isFixedConnection() && connectionManager.getAutoSwitch()) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
if (bestConnection != null) connectionManager.setConnection(bestConnection);
|
||||
} else {
|
||||
connectionManager.checkConnection();
|
||||
}
|
||||
|
||||
// set error message
|
||||
if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
|
||||
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
// set error message
|
||||
getConnectionServiceErrorMsg().set(e.getMessage());
|
||||
} finally {
|
||||
pollInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<MoneroPeer> getOnlinePeers() {
|
||||
return daemon.getPeers().stream()
|
||||
.filter(peer -> peer.isOnline())
|
||||
.collect(Collectors.toList());
|
||||
private boolean isFixedConnection() {
|
||||
return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
|
||||
}
|
||||
|
||||
private boolean isFixedConnection() {
|
||||
return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||
private boolean isCustomConnections() {
|
||||
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||
}
|
||||
|
||||
private boolean isProvidedConnections() {
|
||||
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.xmr.XmrNodeSettings;
|
||||
import haveno.core.xmr.nodes.XmrNodes;
|
||||
import haveno.core.xmr.nodes.XmrNodes.XmrNode;
|
||||
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
|
||||
import java.io.File;
|
||||
@ -55,6 +57,7 @@ public class XmrLocalNode {
|
||||
private MoneroConnectionManager connectionManager;
|
||||
private final Config config;
|
||||
private final Preferences preferences;
|
||||
private final XmrNodes xmrNodes;
|
||||
private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
|
||||
|
||||
// required arguments
|
||||
@ -68,20 +71,14 @@ public class XmrLocalNode {
|
||||
if (!Config.baseCurrencyNetwork().isMainnet()) MONEROD_ARGS.add("--" + Config.baseCurrencyNetwork().getNetwork().toLowerCase());
|
||||
}
|
||||
|
||||
// default rpc ports
|
||||
private static Integer rpcPort;
|
||||
static {
|
||||
if (Config.baseCurrencyNetwork().isMainnet()) rpcPort = 18081;
|
||||
else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081;
|
||||
else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081;
|
||||
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
|
||||
}
|
||||
|
||||
@Inject
|
||||
public XmrLocalNode(Config config, Preferences preferences) {
|
||||
public XmrLocalNode(Config config,
|
||||
Preferences preferences,
|
||||
XmrNodes xmrNodes) {
|
||||
this.config = config;
|
||||
this.preferences = preferences;
|
||||
this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort);
|
||||
this.xmrNodes = xmrNodes;
|
||||
this.daemon = new MoneroDaemonRpc(getUri());
|
||||
|
||||
// initialize connection manager to listen to local connection
|
||||
this.connectionManager = new MoneroConnectionManager().setConnection(daemon.getRpcConnection());
|
||||
@ -92,6 +89,10 @@ public class XmrLocalNode {
|
||||
this.connectionManager.startPolling(REFRESH_PERIOD_LOCAL_MS);
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return "http://" + HavenoUtils.LOOPBACK_HOST + ":" + HavenoUtils.getDefaultMoneroPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Haveno should use a local Monero node, meaning that a node was
|
||||
* detected and conditions under which it should be ignored have not been met. If
|
||||
@ -106,7 +107,20 @@ public class XmrLocalNode {
|
||||
* Returns whether Haveno should ignore a local Monero node even if it is usable.
|
||||
*/
|
||||
public boolean shouldBeIgnored() {
|
||||
return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||
if (config.ignoreLocalXmrNode) return true;
|
||||
|
||||
// ignore if fixed connection is not local
|
||||
if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
|
||||
|
||||
// check if local node is within configuration
|
||||
boolean hasConfiguredLocalNode = false;
|
||||
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||
if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
|
||||
hasConfiguredLocalNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return !hasConfiguredLocalNode;
|
||||
}
|
||||
|
||||
public void addListener(XmrLocalNodeListener listener) {
|
||||
@ -125,7 +139,11 @@ public class XmrLocalNode {
|
||||
}
|
||||
|
||||
public boolean equalsUri(String uri) {
|
||||
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == rpcPort;
|
||||
try {
|
||||
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,33 +173,45 @@ public class XmrLocalNode {
|
||||
/**
|
||||
* Start a local Monero node from settings.
|
||||
*/
|
||||
public void startMoneroNode() throws IOException {
|
||||
public void start() throws IOException {
|
||||
var settings = preferences.getXmrNodeSettings();
|
||||
this.startNode(settings);
|
||||
this.start(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start local Monero node. Throws MoneroError if the node cannot be started.
|
||||
* Persist the settings to preferences if the node started successfully.
|
||||
*/
|
||||
public void startNode(XmrNodeSettings settings) throws IOException {
|
||||
public void start(XmrNodeSettings settings) throws IOException {
|
||||
if (isDetected()) throw new IllegalStateException("Local Monero node already online");
|
||||
|
||||
log.info("Starting local Monero node: " + settings);
|
||||
|
||||
var args = new ArrayList<>(MONEROD_ARGS);
|
||||
|
||||
var dataDir = settings.getBlockchainPath();
|
||||
if (dataDir == null || dataDir.isEmpty()) {
|
||||
dataDir = MONEROD_DATADIR;
|
||||
var dataDir = "";
|
||||
if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
|
||||
dataDir = settings.getBlockchainPath();
|
||||
if (dataDir == null || dataDir.isEmpty()) {
|
||||
dataDir = MONEROD_DATADIR;
|
||||
}
|
||||
} else {
|
||||
dataDir = config.xmrBlockchainPath; // startup config overrides settings
|
||||
}
|
||||
if (dataDir != null && !dataDir.isEmpty()) {
|
||||
args.add("--data-dir=" + dataDir);
|
||||
}
|
||||
if (dataDir != null) args.add("--data-dir=" + dataDir);
|
||||
|
||||
var bootstrapUrl = settings.getBootstrapUrl();
|
||||
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
|
||||
args.add("--bootstrap-daemon-address=" + bootstrapUrl);
|
||||
}
|
||||
|
||||
var syncBlockchain = settings.getSyncBlockchain();
|
||||
if (syncBlockchain != null && !syncBlockchain) {
|
||||
args.add("--no-sync");
|
||||
}
|
||||
|
||||
var flags = settings.getStartupFlags();
|
||||
if (flags != null) {
|
||||
args.addAll(flags);
|
||||
@ -196,7 +226,7 @@ public class XmrLocalNode {
|
||||
* Stop the current local Monero node if we own its process.
|
||||
* Does not remove the last XmrNodeSettings.
|
||||
*/
|
||||
public void stopNode() {
|
||||
public void stop() {
|
||||
if (!isDetected()) throw new IllegalStateException("Local Monero node is not running");
|
||||
if (daemon.getProcess() == null || !daemon.getProcess().isAlive()) throw new IllegalStateException("Cannot stop local Monero node because we don't own its process"); // TODO (woodser): remove isAlive() check after monero-java 0.5.4 which nullifies internal process
|
||||
daemon.stopProcess();
|
||||
|
@ -78,6 +78,9 @@ public class OfferInfo implements Payload {
|
||||
@Nullable
|
||||
private final String splitOutputTxHash;
|
||||
private final long splitOutputTxFee;
|
||||
private final boolean isPrivateOffer;
|
||||
private final String challenge;
|
||||
private final String extraInfo;
|
||||
|
||||
public OfferInfo(OfferInfoBuilder builder) {
|
||||
this.id = builder.getId();
|
||||
@ -111,6 +114,9 @@ public class OfferInfo implements Payload {
|
||||
this.arbitratorSigner = builder.getArbitratorSigner();
|
||||
this.splitOutputTxHash = builder.getSplitOutputTxHash();
|
||||
this.splitOutputTxFee = builder.getSplitOutputTxFee();
|
||||
this.isPrivateOffer = builder.isPrivateOffer();
|
||||
this.challenge = builder.getChallenge();
|
||||
this.extraInfo = builder.getExtraInfo();
|
||||
}
|
||||
|
||||
public static OfferInfo toOfferInfo(Offer offer) {
|
||||
@ -137,6 +143,7 @@ public class OfferInfo implements Payload {
|
||||
.withIsActivated(isActivated)
|
||||
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
|
||||
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
|
||||
.withChallenge(openOffer.getChallenge())
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -177,7 +184,10 @@ public class OfferInfo implements Payload {
|
||||
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
|
||||
.withVersionNumber(offer.getOfferPayload().getVersionNr())
|
||||
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
|
||||
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
|
||||
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
|
||||
.withIsPrivateOffer(offer.isPrivateOffer())
|
||||
.withChallenge(offer.getChallenge())
|
||||
.withExtraInfo(offer.getCombinedExtraInfo());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -215,9 +225,12 @@ public class OfferInfo implements Payload {
|
||||
.setPubKeyRing(pubKeyRing)
|
||||
.setVersionNr(versionNumber)
|
||||
.setProtocolVersion(protocolVersion)
|
||||
.setSplitOutputTxFee(splitOutputTxFee);
|
||||
.setSplitOutputTxFee(splitOutputTxFee)
|
||||
.setIsPrivateOffer(isPrivateOffer);
|
||||
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
|
||||
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
|
||||
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
|
||||
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@ -255,6 +268,9 @@ public class OfferInfo implements Payload {
|
||||
.withArbitratorSigner(proto.getArbitratorSigner())
|
||||
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
|
||||
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
|
||||
.withIsPrivateOffer(proto.getIsPrivateOffer())
|
||||
.withChallenge(proto.getChallenge())
|
||||
.withExtraInfo(proto.getExtraInfo())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,12 @@ public final class PaymentAccountForm implements PersistablePayload {
|
||||
SWIFT,
|
||||
TRANSFERWISE,
|
||||
UPHOLD,
|
||||
ZELLE;
|
||||
ZELLE,
|
||||
AUSTRALIA_PAYID,
|
||||
CASH_APP,
|
||||
PAYPAL,
|
||||
VENMO,
|
||||
PAYSAFE;
|
||||
|
||||
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
|
||||
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());
|
||||
|
@ -98,7 +98,9 @@ public final class PaymentAccountFormField implements PersistablePayload {
|
||||
SPECIAL_INSTRUCTIONS,
|
||||
STATE,
|
||||
TRADE_CURRENCIES,
|
||||
USER_NAME;
|
||||
USERNAME,
|
||||
EMAIL_OR_MOBILE_NR_OR_USERNAME,
|
||||
EMAIL_OR_MOBILE_NR_OR_CASHTAG;
|
||||
|
||||
public static PaymentAccountFormField.FieldId fromProto(protobuf.PaymentAccountFormField.FieldId fieldId) {
|
||||
return ProtoUtil.enumFromProto(PaymentAccountFormField.FieldId.class, fieldId.name());
|
||||
|
@ -21,6 +21,7 @@ import haveno.common.Payload;
|
||||
import haveno.core.api.model.builder.TradeInfoV1Builder;
|
||||
import haveno.core.trade.Contract;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeUtil;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
@ -142,10 +143,7 @@ public class TradeInfo implements Payload {
|
||||
}
|
||||
|
||||
public static TradeInfo toTradeInfo(Trade trade) {
|
||||
return toTradeInfo(trade, null);
|
||||
}
|
||||
|
||||
public static TradeInfo toTradeInfo(Trade trade, String role) {
|
||||
String role = TradeUtil.getRole(trade);
|
||||
ContractInfo contractInfo;
|
||||
if (trade.getContract() != null) {
|
||||
Contract contract = trade.getContract();
|
||||
@ -174,14 +172,14 @@ public class TradeInfo implements Payload {
|
||||
.withAmount(trade.getAmount().longValueExact())
|
||||
.withMakerFee(trade.getMakerFee().longValueExact())
|
||||
.withTakerFee(trade.getTakerFee().longValueExact())
|
||||
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
|
||||
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
|
||||
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
|
||||
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
|
||||
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
|
||||
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
|
||||
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
|
||||
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
|
||||
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
|
||||
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
|
||||
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
|
||||
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
|
||||
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
|
||||
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
|
||||
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
|
||||
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
|
||||
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
|
||||
.withPrice(toPreciseTradePrice.apply(trade))
|
||||
.withVolume(toRoundedVolume.apply(trade))
|
||||
|
@ -98,7 +98,7 @@ public class XmrBalanceInfo implements Payload {
|
||||
public String toString() {
|
||||
return "XmrBalanceInfo{" +
|
||||
"balance=" + balance +
|
||||
"unlockedBalance=" + availableBalance +
|
||||
", unlockedBalance=" + availableBalance +
|
||||
", lockedBalance=" + pendingBalance +
|
||||
", reservedOfferBalance=" + reservedOfferBalance +
|
||||
", reservedTradeBalance=" + reservedTradeBalance +
|
||||
|
@ -63,6 +63,9 @@ public final class OfferInfoBuilder {
|
||||
private String arbitratorSigner;
|
||||
private String splitOutputTxHash;
|
||||
private long splitOutputTxFee;
|
||||
private boolean isPrivateOffer;
|
||||
private String challenge;
|
||||
private String extraInfo;
|
||||
|
||||
public OfferInfoBuilder withId(String id) {
|
||||
this.id = id;
|
||||
@ -234,6 +237,21 @@ public final class OfferInfoBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) {
|
||||
this.isPrivateOffer = isPrivateOffer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfoBuilder withChallenge(String challenge) {
|
||||
this.challenge = challenge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfoBuilder withExtraInfo(String extraInfo) {
|
||||
this.extraInfo = extraInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfo build() {
|
||||
return new OfferInfo(this);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ public class AppStartupState {
|
||||
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
public void onDataReceived() {
|
||||
updatedDataReceived.set(true);
|
||||
}
|
||||
});
|
||||
@ -73,7 +73,7 @@ public class AppStartupState {
|
||||
isWalletSynced.set(true);
|
||||
});
|
||||
|
||||
xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (xmrConnectionService.hasSufficientPeersForBroadcast())
|
||||
hasSufficientPeersForBroadcast.set(true);
|
||||
});
|
||||
|
@ -178,6 +178,9 @@ public class DomainInitialisation {
|
||||
closedTradableManager.onAllServicesInitialized();
|
||||
failedTradesManager.onAllServicesInitialized();
|
||||
|
||||
filterManager.setFilterWarningHandler(filterWarningHandler);
|
||||
filterManager.onAllServicesInitialized();
|
||||
|
||||
openOfferManager.onAllServicesInitialized();
|
||||
|
||||
balances.onAllServicesInitialized();
|
||||
@ -199,10 +202,6 @@ public class DomainInitialisation {
|
||||
priceFeedService.setCurrencyCodeOnInit();
|
||||
priceFeedService.startRequestingPrices();
|
||||
|
||||
filterManager.setFilterWarningHandler(filterWarningHandler);
|
||||
filterManager.onAllServicesInitialized();
|
||||
|
||||
|
||||
mobileNotificationService.onAllServicesInitialized();
|
||||
myOfferTakenEvents.onAllServicesInitialized();
|
||||
tradeEvents.onAllServicesInitialized();
|
||||
@ -217,7 +216,7 @@ public class DomainInitialisation {
|
||||
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()
|
||||
.filter(paymentAccount -> paymentAccount instanceof RevolutAccount)
|
||||
.map(paymentAccount -> (RevolutAccount) paymentAccount)
|
||||
.filter(RevolutAccount::userNameNotSet)
|
||||
.filter(RevolutAccount::usernameNotSet)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
if (amazonGiftCardAccountsUpdateHandler != null && user.getPaymentAccountsAsObservable() != null) {
|
||||
|
@ -82,6 +82,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
@Slf4j
|
||||
public abstract class HavenoExecutable implements GracefulShutDownHandler, HavenoSetup.HavenoSetupListener, UncaughtExceptionHandler {
|
||||
|
||||
// TODO: regular expression is used to parse application name for the flatpak manifest, a more stable approach would be nice
|
||||
// Don't edit the next line unless you're only editing in between the quotes.
|
||||
public static final String DEFAULT_APP_NAME = "Haveno";
|
||||
|
||||
public static final int EXIT_SUCCESS = 0;
|
||||
public static final int EXIT_FAILURE = 1;
|
||||
public static final int EXIT_RESTART = 2;
|
||||
@ -96,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
protected AppModule module;
|
||||
protected Config config;
|
||||
@Getter
|
||||
protected boolean isShutdownInProgress;
|
||||
protected boolean isShutDownStarted;
|
||||
private boolean isReadOnly;
|
||||
private Thread keepRunningThread;
|
||||
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
|
||||
@ -122,7 +126,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
System.exit(EXIT_FAILURE);
|
||||
} catch (Throwable ex) {
|
||||
System.err.println("fault: An unexpected error occurred. " +
|
||||
"Please file a report at https://haveno.exchange/issues");
|
||||
"Please file a report at https://github.com/haveno-dex/haveno/issues");
|
||||
ex.printStackTrace(System.err);
|
||||
System.exit(EXIT_FAILURE);
|
||||
}
|
||||
@ -199,8 +203,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
startApplication();
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
log.error("An error occurred: {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("An error occurred: {}\n", e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -327,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
|
||||
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||
|
||||
// ignore if shut down in progress
|
||||
if (isShutdownInProgress) {
|
||||
log.info("Ignoring call to gracefulShutDown, already in progress");
|
||||
// ignore if shut down started
|
||||
if (isShutDownStarted) {
|
||||
log.info("Ignoring call to gracefulShutDown, already started");
|
||||
return;
|
||||
}
|
||||
isShutdownInProgress = true;
|
||||
isShutDownStarted = true;
|
||||
|
||||
ResultHandler resultHandler;
|
||||
if (shutdownCompletedHandler != null) {
|
||||
@ -354,49 +357,49 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
|
||||
// notify trade protocols and wallets to prepare for shut down before shutting down
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||
try {
|
||||
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e);
|
||||
}
|
||||
|
||||
injector.getInstance(TradeManager.class).shutDown();
|
||||
injector.getInstance(PriceFeedService.class).shutDown();
|
||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||
|
||||
// shut down open offer manager
|
||||
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||
log.info("Shutting down OpenOfferManager");
|
||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||
|
||||
// shut down offer book service
|
||||
injector.getInstance(OfferBookService.class).shutDown();
|
||||
// listen for shut down of wallets setup
|
||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||
|
||||
// shut down p2p service
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
// shut down p2p service
|
||||
log.info("Shutting down P2P service");
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
|
||||
// shut down monero wallets and connections
|
||||
log.info("Shutting down wallet and connection services");
|
||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||
|
||||
// done shutting down
|
||||
log.info("Graceful shutdown completed. Exiting now.");
|
||||
module.close(injector);
|
||||
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
|
||||
});
|
||||
injector.getInstance(BtcWalletService.class).shutDown();
|
||||
injector.getInstance(XmrWalletService.class).shutDown();
|
||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||
injector.getInstance(WalletsSetup.class).shutDown();
|
||||
});
|
||||
|
||||
// shut down trade and wallet services
|
||||
log.info("Shutting down trade and wallet services");
|
||||
injector.getInstance(OfferBookService.class).shutDown();
|
||||
injector.getInstance(TradeManager.class).shutDown();
|
||||
injector.getInstance(BtcWalletService.class).shutDown();
|
||||
injector.getInstance(XmrWalletService.class).shutDown();
|
||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||
injector.getInstance(WalletsSetup.class).shutDown();
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
log.error("App shutdown failed with exception {}", t.toString());
|
||||
t.printStackTrace();
|
||||
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
|
||||
completeShutdown(resultHandler, EXIT_FAILURE, systemExit);
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +75,10 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
|
||||
acceptedHandler.run();
|
||||
});
|
||||
havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show));
|
||||
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
|
||||
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
|
||||
havenoSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
|
||||
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
|
||||
havenoSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler"));
|
||||
havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler"));
|
||||
havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert));
|
||||
@ -85,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
|
||||
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
|
||||
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
|
||||
havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
|
||||
havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler"));
|
||||
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
|
||||
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
|
||||
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
|
||||
|
@ -31,7 +31,7 @@ public class HavenoHeadlessAppMain extends HavenoExecutable {
|
||||
protected HeadlessApp headlessApp;
|
||||
|
||||
public HavenoHeadlessAppMain() {
|
||||
super("Haveno Daemon", "havenod", "Haveno", Version.VERSION);
|
||||
super("Haveno Daemon", "havenod", HavenoExecutable.DEFAULT_APP_NAME, Version.VERSION);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
@ -53,6 +53,9 @@ import haveno.core.alert.Alert;
|
||||
import haveno.core.alert.AlertManager;
|
||||
import haveno.core.alert.PrivateNotificationManager;
|
||||
import haveno.core.alert.PrivateNotificationPayload;
|
||||
import haveno.core.api.CoreContext;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
|
||||
import haveno.core.api.XmrLocalNode;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
@ -66,15 +69,12 @@ import haveno.core.support.dispute.mediation.MediationManager;
|
||||
import haveno.core.support.dispute.refund.RefundManager;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.TradeTxException;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.Preferences.UseTorForXmr;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.FormattingUtils;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import haveno.core.xmr.model.AddressEntry;
|
||||
import haveno.core.xmr.setup.WalletsSetup;
|
||||
import haveno.core.xmr.wallet.BtcWalletService;
|
||||
import haveno.core.xmr.wallet.WalletsManager;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.Socks5ProxyProvider;
|
||||
@ -92,7 +92,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
@ -100,6 +99,7 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.SetChangeListener;
|
||||
@ -107,7 +107,6 @@ import javax.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
|
||||
@ -116,14 +115,14 @@ import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
public class HavenoSetup {
|
||||
private static final String VERSION_FILE_NAME = "version";
|
||||
|
||||
private static final long STARTUP_TIMEOUT_MINUTES = 5;
|
||||
private static final long STARTUP_TIMEOUT_MINUTES = 4;
|
||||
|
||||
private final DomainInitialisation domainInitialisation;
|
||||
private final P2PNetworkSetup p2PNetworkSetup;
|
||||
private final WalletAppSetup walletAppSetup;
|
||||
private final WalletsManager walletsManager;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final XmrConnectionService xmrConnectionService;
|
||||
@Getter
|
||||
private final XmrWalletService xmrWalletService;
|
||||
private final P2PService p2PService;
|
||||
@ -134,7 +133,10 @@ public class HavenoSetup {
|
||||
private final Preferences preferences;
|
||||
private final User user;
|
||||
private final AlertManager alertManager;
|
||||
@Getter
|
||||
private final Config config;
|
||||
@Getter
|
||||
private final CoreContext coreContext;
|
||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
private final TorSetup torSetup;
|
||||
private final CoinFormatter formatter;
|
||||
@ -143,6 +145,7 @@ public class HavenoSetup {
|
||||
private final MediationManager mediationManager;
|
||||
private final RefundManager refundManager;
|
||||
private final ArbitrationManager arbitrationManager;
|
||||
private final StringProperty topErrorMsg = new SimpleStringProperty();
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<Runnable> displayTacHandler;
|
||||
@ -156,6 +159,9 @@ public class HavenoSetup {
|
||||
rejectedTxErrorMessageHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
@ -171,7 +177,7 @@ public class HavenoSetup {
|
||||
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Runnable showPopupIfInvalidBtcConfigHandler;
|
||||
private Runnable showPopupIfInvalidXmrConfigHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
|
||||
@ -219,8 +225,8 @@ public class HavenoSetup {
|
||||
WalletAppSetup walletAppSetup,
|
||||
WalletsManager walletsManager,
|
||||
WalletsSetup walletsSetup,
|
||||
XmrConnectionService xmrConnectionService,
|
||||
XmrWalletService xmrWalletService,
|
||||
BtcWalletService btcWalletService,
|
||||
P2PService p2PService,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
SignedWitnessStorageService signedWitnessStorageService,
|
||||
@ -230,6 +236,7 @@ public class HavenoSetup {
|
||||
User user,
|
||||
AlertManager alertManager,
|
||||
Config config,
|
||||
CoreContext coreContext,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
TorSetup torSetup,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
@ -244,8 +251,8 @@ public class HavenoSetup {
|
||||
this.walletAppSetup = walletAppSetup;
|
||||
this.walletsManager = walletsManager;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.xmrConnectionService = xmrConnectionService;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.p2PService = p2PService;
|
||||
this.privateNotificationManager = privateNotificationManager;
|
||||
this.signedWitnessStorageService = signedWitnessStorageService;
|
||||
@ -255,6 +262,7 @@ public class HavenoSetup {
|
||||
this.user = user;
|
||||
this.alertManager = alertManager;
|
||||
this.config = config;
|
||||
this.coreContext = coreContext;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.torSetup = torSetup;
|
||||
this.formatter = formatter;
|
||||
@ -265,6 +273,7 @@ public class HavenoSetup {
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
|
||||
HavenoUtils.havenoSetup = this;
|
||||
HavenoUtils.preferences = preferences;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -361,7 +370,7 @@ public class HavenoSetup {
|
||||
// install monerod
|
||||
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
|
||||
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
|
||||
if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) {
|
||||
if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) {
|
||||
log.info("Installing monerod");
|
||||
monerodFile.getParentFile().mkdirs();
|
||||
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
|
||||
@ -371,15 +380,14 @@ public class HavenoSetup {
|
||||
// install monero-wallet-rpc
|
||||
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
|
||||
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
|
||||
if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) {
|
||||
if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) {
|
||||
log.info("Installing monero-wallet-rpc");
|
||||
moneroWalletRpcFile.getParentFile().mkdirs();
|
||||
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
|
||||
moneroWalletRpcFile.setExecutable(true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error(e.toString());
|
||||
log.warn("Failed to install Monero binaries: {}\n", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,6 +430,12 @@ public class HavenoSetup {
|
||||
getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||
|
||||
// listen for fallback handling
|
||||
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
|
||||
if (displayMoneroConnectionFallbackHandler == null) return;
|
||||
displayMoneroConnectionFallbackHandler.accept(newValue);
|
||||
});
|
||||
|
||||
log.info("Init P2P network");
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
|
||||
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
|
||||
@ -448,12 +462,8 @@ public class HavenoSetup {
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
|
||||
walletAppSetup.init(chainFileLockedExceptionHandler,
|
||||
showFirstPopupIfResyncSPVRequestedHandler,
|
||||
showPopupIfInvalidBtcConfigHandler,
|
||||
() -> {
|
||||
if (allBasicServicesInitialized) {
|
||||
checkForLockedUpFunds();
|
||||
}
|
||||
},
|
||||
showPopupIfInvalidXmrConfigHandler,
|
||||
() -> {},
|
||||
() -> {});
|
||||
}
|
||||
|
||||
@ -466,10 +476,6 @@ public class HavenoSetup {
|
||||
revolutAccountsUpdateHandler,
|
||||
amazonGiftCardAccountsUpdateHandler);
|
||||
|
||||
if (xmrWalletService.downloadPercentageProperty().get() == 1) {
|
||||
checkForLockedUpFunds();
|
||||
}
|
||||
|
||||
alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) ->
|
||||
displayAlertIfPresent(newValue, false));
|
||||
displayAlertIfPresent(alertManager.alertMessageProperty().get(), false);
|
||||
@ -484,32 +490,6 @@ public class HavenoSetup {
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void checkForLockedUpFunds() {
|
||||
// We check if there are locked up funds in failed or closed trades
|
||||
try {
|
||||
Set<String> setOfAllTradeIds = tradeManager.getSetOfFailedOrClosedTradeIdsFromLockedInFunds();
|
||||
btcWalletService.getAddressEntriesForTrade().stream()
|
||||
.filter(e -> setOfAllTradeIds.contains(e.getOfferId()) &&
|
||||
e.getContext() == AddressEntry.Context.MULTI_SIG)
|
||||
.forEach(e -> {
|
||||
Coin balance = e.getCoinLockedInMultiSigAsCoin();
|
||||
if (balance.isPositive()) {
|
||||
String message = Res.get("popup.warning.lockedUpFunds",
|
||||
formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId());
|
||||
log.warn(message);
|
||||
if (lockedUpFundsHandler != null) {
|
||||
lockedUpFundsHandler.accept(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (TradeTxException e) {
|
||||
log.warn(e.getMessage());
|
||||
if (lockedUpFundsHandler != null) {
|
||||
lockedUpFundsHandler.accept(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLastHavenoVersion() {
|
||||
File versionFile = getVersionFile();
|
||||
@ -751,8 +731,16 @@ public class HavenoSetup {
|
||||
return walletAppSetup.getXmrWalletSyncProgress();
|
||||
}
|
||||
|
||||
public StringProperty getWalletServiceErrorMsg() {
|
||||
return walletAppSetup.getWalletServiceErrorMsg();
|
||||
public StringProperty getConnectionServiceErrorMsg() {
|
||||
return xmrConnectionService.getConnectionServiceErrorMsg();
|
||||
}
|
||||
|
||||
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
|
||||
return xmrConnectionService.getConnectionServiceFallbackType();
|
||||
}
|
||||
|
||||
public StringProperty getTopErrorMsg() {
|
||||
return topErrorMsg;
|
||||
}
|
||||
|
||||
public StringProperty getXmrSplashSyncIconId() {
|
||||
|
@ -87,7 +87,7 @@ public class P2PNetworkSetup {
|
||||
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
|
||||
|
||||
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
|
||||
xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
|
||||
String result;
|
||||
int p2pPeers = (int) numP2pPeers;
|
||||
|
@ -28,6 +28,9 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@ -48,8 +51,7 @@ public class TorSetup {
|
||||
if (resultHandler != null)
|
||||
resultHandler.run();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error(e.toString());
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
if (errorMessageHandler != null)
|
||||
errorMessageHandler.handleErrorMessage(e.toString());
|
||||
}
|
||||
|
@ -89,8 +89,6 @@ public class WalletAppSetup {
|
||||
@Getter
|
||||
private final DoubleProperty xmrWalletSyncProgress = new SimpleDoubleProperty(-1);
|
||||
@Getter
|
||||
private final StringProperty walletServiceErrorMsg = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final StringProperty xmrSplashSyncIconId = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final StringProperty xmrInfo = new SimpleStringProperty(Res.get("mainView.footer.xmrInfo.initializing"));
|
||||
@ -119,10 +117,10 @@ public class WalletAppSetup {
|
||||
|
||||
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
|
||||
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
|
||||
@Nullable Runnable showPopupIfInvalidBtcConfigHandler,
|
||||
@Nullable Runnable showPopupIfInvalidXmrConfigHandler,
|
||||
Runnable downloadCompleteHandler,
|
||||
Runnable walletInitializedHandler) {
|
||||
log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion());
|
||||
log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
|
||||
|
||||
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
|
||||
xmrInfoBinding = EasyBind.combine(
|
||||
@ -130,39 +128,18 @@ public class WalletAppSetup {
|
||||
xmrWalletService.downloadPercentageProperty(),
|
||||
xmrWalletService.walletHeightProperty(),
|
||||
walletServiceException,
|
||||
getWalletServiceErrorMsg(),
|
||||
xmrConnectionService.getConnectionServiceErrorMsg(),
|
||||
(numConnectionUpdates, walletDownloadPercentage, walletHeight, exception, errorMsg) -> {
|
||||
String result;
|
||||
if (exception == null && errorMsg == null) {
|
||||
|
||||
// update wallet sync progress
|
||||
double walletDownloadPercentageD = (double) walletDownloadPercentage;
|
||||
xmrWalletSyncProgress.set(walletDownloadPercentageD);
|
||||
Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight;
|
||||
String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : "";
|
||||
if (walletDownloadPercentageD == 1) {
|
||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString);
|
||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||
downloadCompleteHandler.run();
|
||||
} else if (walletDownloadPercentageD > 0) {
|
||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||
getXmrSplashSyncIconId().set(""); // clear synced icon
|
||||
} else {
|
||||
|
||||
// update daemon sync progress
|
||||
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
|
||||
|
||||
// update daemon sync progress
|
||||
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
|
||||
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
|
||||
if (chainDownloadPercentageD < 1) {
|
||||
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
|
||||
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
|
||||
if (chainDownloadPercentageD == 1) {
|
||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
|
||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||
} else if (chainDownloadPercentageD > 0.0) {
|
||||
if (chainDownloadPercentageD > 0.0) {
|
||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||
} else {
|
||||
@ -170,6 +147,29 @@ public class WalletAppSetup {
|
||||
Res.get("mainView.footer.xmrInfo.connectingTo"),
|
||||
getXmrDaemonNetworkAsString());
|
||||
}
|
||||
} else {
|
||||
|
||||
// update wallet sync progress
|
||||
double walletDownloadPercentageD = (double) walletDownloadPercentage;
|
||||
xmrWalletSyncProgress.set(walletDownloadPercentageD);
|
||||
Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight;
|
||||
String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : "";
|
||||
if (walletDownloadPercentageD == 1) {
|
||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString);
|
||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||
downloadCompleteHandler.run();
|
||||
} else if (walletDownloadPercentageD >= 0) {
|
||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||
getXmrSplashSyncIconId().set(""); // clear synced icon
|
||||
} else {
|
||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
|
||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = Res.get("mainView.footer.xmrInfo",
|
||||
@ -177,16 +177,16 @@ public class WalletAppSetup {
|
||||
getXmrDaemonNetworkAsString());
|
||||
if (exception != null) {
|
||||
if (exception instanceof TimeoutException) {
|
||||
getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.timeout"));
|
||||
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.timeout"));
|
||||
} else if (exception.getCause() instanceof BlockStoreException) {
|
||||
if (exception.getCause().getCause() instanceof ChainFileLockedException && chainFileLockedExceptionHandler != null) {
|
||||
chainFileLockedExceptionHandler.accept(Res.get("popup.warning.startupFailed.twoInstances"));
|
||||
}
|
||||
} else if (exception instanceof RejectedTxException) {
|
||||
rejectedTxException.set((RejectedTxException) exception);
|
||||
getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.rejectedTxException", exception.getMessage()));
|
||||
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.rejectedTxException", exception.getMessage()));
|
||||
} else {
|
||||
getWalletServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage()));
|
||||
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,8 +199,8 @@ public class WalletAppSetup {
|
||||
walletInitializedHandler.run();
|
||||
},
|
||||
exception -> {
|
||||
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
|
||||
showPopupIfInvalidBtcConfigHandler.run();
|
||||
if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
|
||||
showPopupIfInvalidXmrConfigHandler.run();
|
||||
} else {
|
||||
walletServiceException.set(exception);
|
||||
}
|
||||
@ -270,9 +270,9 @@ public class WalletAppSetup {
|
||||
|
||||
private String getXmrDaemonNetworkAsString() {
|
||||
String postFix;
|
||||
if (xmrConnectionService.isConnectionLocal())
|
||||
if (xmrConnectionService.isConnectionLocalHost())
|
||||
postFix = " " + Res.get("mainView.footer.localhostMoneroNode");
|
||||
else if (xmrConnectionService.isConnectionTor())
|
||||
else if (xmrConnectionService.isProxyApplied())
|
||||
postFix = " " + Res.get("mainView.footer.usingTor");
|
||||
else
|
||||
postFix = "";
|
||||
@ -281,7 +281,7 @@ public class WalletAppSetup {
|
||||
|
||||
private String getXmrWalletNetworkAsString() {
|
||||
String postFix;
|
||||
if (xmrConnectionService.isConnectionLocal())
|
||||
if (xmrConnectionService.isConnectionLocalHost())
|
||||
postFix = " " + Res.get("mainView.footer.localhostMoneroNode");
|
||||
else if (xmrWalletService.isProxyApplied())
|
||||
postFix = " " + Res.get("mainView.footer.usingTor");
|
||||
|
@ -105,47 +105,43 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
||||
public void gracefulShutDown(ResultHandler resultHandler) {
|
||||
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||
|
||||
// ignore if shut down in progress
|
||||
if (isShutdownInProgress) {
|
||||
log.info("Ignoring call to gracefulShutDown, already in progress");
|
||||
// ignore if shut down started
|
||||
if (isShutDownStarted) {
|
||||
log.info("Ignoring call to gracefulShutDown, already started");
|
||||
return;
|
||||
}
|
||||
isShutdownInProgress = true;
|
||||
isShutDownStarted = true;
|
||||
|
||||
try {
|
||||
if (injector != null) {
|
||||
|
||||
// notify trade protocols and wallets to prepare for shut down
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||
try {
|
||||
ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("Error awaiting tasks to complete: {}\n", e.getMessage(), e);
|
||||
}
|
||||
|
||||
JsonFileManager.shutDownAllInstances();
|
||||
injector.getInstance(TradeManager.class).shutDown();
|
||||
injector.getInstance(PriceFeedService.class).shutDown();
|
||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||
|
||||
// shut down open offer manager
|
||||
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||
log.info("Shutting down OpenOfferManager");
|
||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||
|
||||
// shut down offer book service
|
||||
injector.getInstance(OfferBookService.class).shutDown();
|
||||
// listen for shut down of wallets setup
|
||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||
|
||||
// shut down p2p service
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
|
||||
// shut down monero wallets and connections
|
||||
log.info("Shutting down wallet and connection services");
|
||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||
// shut down p2p service
|
||||
log.info("Shutting down P2P service");
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
module.close(injector);
|
||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||
|
||||
@ -155,6 +151,11 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
||||
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
||||
});
|
||||
});
|
||||
|
||||
// shut down trade and wallet services
|
||||
log.info("Shutting down trade and wallet services");
|
||||
injector.getInstance(OfferBookService.class).shutDown();
|
||||
injector.getInstance(TradeManager.class).shutDown();
|
||||
injector.getInstance(BtcWalletService.class).shutDown();
|
||||
injector.getInstance(XmrWalletService.class).shutDown();
|
||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||
@ -177,8 +178,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
||||
}, 1);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log.debug("App shutdown failed with exception");
|
||||
t.printStackTrace();
|
||||
log.info("App shutdown failed with exception: {}\n", t.getMessage(), t);
|
||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||
resultHandler.handleResult();
|
||||
log.info("Graceful shutdown resulted in an error. Exiting now.");
|
||||
|
@ -406,6 +406,10 @@ public class FilterManager {
|
||||
.anyMatch(e -> e.equals(address));
|
||||
}
|
||||
|
||||
public String getDisableTradeBelowVersion() {
|
||||
return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
|
||||
}
|
||||
|
||||
public boolean requireUpdateToNewVersionForTrading() {
|
||||
if (getFilter() == null) {
|
||||
return false;
|
||||
|
@ -19,10 +19,8 @@ package haveno.core.locale;
|
||||
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class CryptoCurrency extends TradeCurrency {
|
||||
// http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618
|
||||
private final static String PREFIX = "✦ ";
|
||||
|
@ -73,14 +73,6 @@ public class CurrencyUtil {
|
||||
|
||||
private static String baseCurrencyCode = "XMR";
|
||||
|
||||
private static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
|
||||
return Arrays.asList(
|
||||
new TraditionalCurrency("XAG", "Silver"),
|
||||
new TraditionalCurrency("XAU", "Gold"),
|
||||
new TraditionalCurrency("XGB", "Goldback")
|
||||
);
|
||||
}
|
||||
|
||||
// Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results.
|
||||
// The main improvement was already achieved with using memoize for the source maps, but
|
||||
// the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50%
|
||||
@ -124,6 +116,14 @@ public class CurrencyUtil {
|
||||
return new ArrayList<>(traditionalCurrencyMapSupplier.get().values());
|
||||
}
|
||||
|
||||
public static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
|
||||
return Arrays.asList(
|
||||
new TraditionalCurrency("XAG", "Silver"),
|
||||
new TraditionalCurrency("XAU", "Gold"),
|
||||
new TraditionalCurrency("XGB", "Goldback")
|
||||
);
|
||||
}
|
||||
|
||||
public static Collection<TraditionalCurrency> getAllSortedTraditionalCurrencies(Comparator comparator) {
|
||||
return (List<TraditionalCurrency>) getAllSortedTraditionalCurrencies().stream()
|
||||
.sorted(comparator)
|
||||
@ -200,6 +200,10 @@ public class CurrencyUtil {
|
||||
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
|
||||
result.add(new CryptoCurrency("ETH", "Ether"));
|
||||
result.add(new CryptoCurrency("LTC", "Litecoin"));
|
||||
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)"));
|
||||
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
|
||||
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)"));
|
||||
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)"));
|
||||
result.sort(TradeCurrency::compareTo);
|
||||
return result;
|
||||
}
|
||||
@ -295,6 +299,9 @@ public class CurrencyUtil {
|
||||
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
|
||||
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
|
||||
}
|
||||
if (isCryptoCurrencyCodeBase(currencyCode)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isCryptoCurrency;
|
||||
if (currencyCode == null) {
|
||||
@ -321,6 +328,21 @@ public class CurrencyUtil {
|
||||
return isCryptoCurrency;
|
||||
}
|
||||
|
||||
private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
|
||||
if (currencyCode == null) return false;
|
||||
currencyCode = currencyCode.toUpperCase();
|
||||
return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
|
||||
}
|
||||
|
||||
public static String getCurrencyCodeBase(String currencyCode) {
|
||||
if (currencyCode == null) return null;
|
||||
currencyCode = currencyCode.toUpperCase();
|
||||
if (currencyCode.contains("USDT")) return "USDT";
|
||||
if (currencyCode.contains("USDC")) return "USDC";
|
||||
if (currencyCode.contains("DAI")) return "DAI";
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
public static Optional<CryptoCurrency> getCryptoCurrency(String currencyCode) {
|
||||
return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode));
|
||||
}
|
||||
|
@ -44,14 +44,14 @@ public class LanguageUtil {
|
||||
"fa", // Persian
|
||||
"it", // Italian
|
||||
"cs", // Czech
|
||||
"pl" // Polish
|
||||
"pl", // Polish
|
||||
"tr" // Turkish
|
||||
/*
|
||||
// not translated yet
|
||||
"el", // Greek
|
||||
"sr-Latn-RS", // Serbian [Latin] (Serbia)
|
||||
"hu", // Hungarian
|
||||
"ro", // Romanian
|
||||
"tr" // Turkish
|
||||
"iw", // Hebrew
|
||||
"hi", // Hindi
|
||||
"ko", // Korean
|
||||
|
@ -19,19 +19,16 @@ package haveno.core.locale;
|
||||
|
||||
import haveno.common.proto.ProtobufferRuntimeException;
|
||||
import haveno.common.proto.persistable.PersistablePayload;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@Getter
|
||||
@Slf4j
|
||||
public abstract class TradeCurrency implements PersistablePayload, Comparable<TradeCurrency> {
|
||||
protected final String code;
|
||||
@EqualsAndHashCode.Exclude
|
||||
protected final String name;
|
||||
|
||||
public TradeCurrency(String code, String name) {
|
||||
@ -82,4 +79,23 @@ public abstract class TradeCurrency implements PersistablePayload, Comparable<Tr
|
||||
return this.name.compareTo(other.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (obj instanceof TradeCurrency) {
|
||||
TradeCurrency other = (TradeCurrency) obj;
|
||||
return code.equals(other.code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return code.hashCode();
|
||||
}
|
||||
}
|
||||
|
@ -36,14 +36,12 @@ package haveno.core.locale;
|
||||
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Currency;
|
||||
import java.util.Locale;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString
|
||||
@Getter
|
||||
public final class TraditionalCurrency extends TradeCurrency {
|
||||
|
@ -114,16 +114,18 @@ public class DisputeMsgEvents {
|
||||
// We check at every new message if it might be a message sent after the dispute had been closed. If that is the
|
||||
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
|
||||
// message arrived.
|
||||
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
|
||||
// If last message is not a result message we re-open as we might have received a new message from the
|
||||
// trader/mediator/arbitrator who has reopened the case
|
||||
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
|
||||
dispute.reOpen();
|
||||
if (dispute.getSupportType() == SupportType.MEDIATION) {
|
||||
mediationManager.requestPersistence();
|
||||
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
||||
refundManager.requestPersistence();
|
||||
synchronized (dispute.getChatMessages()) {
|
||||
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
|
||||
// If last message is not a result message we re-open as we might have received a new message from the
|
||||
// trader/mediator/arbitrator who has reopened the case
|
||||
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
|
||||
dispute.reOpen();
|
||||
if (dispute.getSupportType() == SupportType.MEDIATION) {
|
||||
mediationManager.requestPersistence();
|
||||
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
||||
refundManager.requestPersistence();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.coin.CoinUtil;
|
||||
import haveno.core.xmr.wallet.Restrictions;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.P2PService;
|
||||
@ -94,6 +92,7 @@ public class CreateOfferService {
|
||||
Version.VERSION.replace(".", "");
|
||||
}
|
||||
|
||||
// TODO: add trigger price?
|
||||
public Offer createAndGetOffer(String offerId,
|
||||
OfferDirection direction,
|
||||
String currencyCode,
|
||||
@ -102,10 +101,12 @@ public class CreateOfferService {
|
||||
Price fixedPrice,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
double securityDepositAsDouble,
|
||||
PaymentAccount paymentAccount) {
|
||||
|
||||
log.info("create and get offer with offerId={}, " +
|
||||
double securityDepositPct,
|
||||
PaymentAccount paymentAccount,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo) {
|
||||
log.info("Create and get offer with offerId={}, " +
|
||||
"currencyCode={}, " +
|
||||
"direction={}, " +
|
||||
"fixedPrice={}, " +
|
||||
@ -113,7 +114,10 @@ public class CreateOfferService {
|
||||
"marketPriceMargin={}, " +
|
||||
"amount={}, " +
|
||||
"minAmount={}, " +
|
||||
"securityDeposit={}",
|
||||
"securityDepositPct={}, " +
|
||||
"isPrivateOffer={}, " +
|
||||
"buyerAsTakerWithoutDeposit={}, " +
|
||||
"extraInfo={}",
|
||||
offerId,
|
||||
currencyCode,
|
||||
direction,
|
||||
@ -122,7 +126,19 @@ public class CreateOfferService {
|
||||
marketPriceMargin,
|
||||
amount,
|
||||
minAmount,
|
||||
securityDepositAsDouble);
|
||||
securityDepositPct,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
|
||||
// must nullify empty string so contracts match
|
||||
if ("".equals(extraInfo)) extraInfo = null;
|
||||
|
||||
// verify buyer as taker security deposit
|
||||
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
|
||||
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
|
||||
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
|
||||
}
|
||||
|
||||
// verify fixed price xor market price with margin
|
||||
if (fixedPrice != null) {
|
||||
@ -130,25 +146,29 @@ public class CreateOfferService {
|
||||
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
|
||||
}
|
||||
|
||||
long creationTime = new Date().getTime();
|
||||
NodeAddress makerAddress = p2PService.getAddress();
|
||||
// verify price
|
||||
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
||||
useMarketBasedPrice &&
|
||||
isMarketPriceAvailable(currencyCode) &&
|
||||
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
||||
|
||||
// verify price
|
||||
if (fixedPrice == null && !useMarketBasedPriceValue) {
|
||||
throw new IllegalArgumentException("Must provide fixed price");
|
||||
}
|
||||
|
||||
// adjust amount and min amount for fixed-price offer
|
||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
|
||||
if (fixedPrice != null) {
|
||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
// adjust amount and min amount
|
||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
|
||||
// generate one-time challenge for private offer
|
||||
String challenge = null;
|
||||
String challengeHash = null;
|
||||
if (isPrivateOffer) {
|
||||
challenge = HavenoUtils.generateChallenge();
|
||||
challengeHash = HavenoUtils.getChallengeHash(challenge);
|
||||
}
|
||||
|
||||
long creationTime = new Date().getTime();
|
||||
NodeAddress makerAddress = p2PService.getAddress();
|
||||
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
|
||||
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
||||
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
||||
@ -161,21 +181,16 @@ public class CreateOfferService {
|
||||
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
|
||||
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
|
||||
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
|
||||
|
||||
// reserved for future use cases
|
||||
// Use null values if not set
|
||||
boolean isPrivateOffer = false;
|
||||
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
|
||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
|
||||
boolean useAutoClose = false;
|
||||
boolean useReOpenAfterAutoClose = false;
|
||||
long lowerClosePrice = 0;
|
||||
long upperClosePrice = 0;
|
||||
String hashOfChallenge = null;
|
||||
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
|
||||
currencyCode,
|
||||
direction);
|
||||
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
|
||||
|
||||
offerUtil.validateOfferData(
|
||||
securityDepositAsDouble,
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
currencyCode);
|
||||
|
||||
@ -189,11 +204,11 @@ public class CreateOfferService {
|
||||
useMarketBasedPriceValue,
|
||||
amountAsLong,
|
||||
minAmountAsLong,
|
||||
HavenoUtils.MAKER_FEE_PCT,
|
||||
HavenoUtils.TAKER_FEE_PCT,
|
||||
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
|
||||
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
|
||||
HavenoUtils.PENALTY_FEE_PCT,
|
||||
securityDepositAsDouble,
|
||||
securityDepositAsDouble,
|
||||
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
|
||||
securityDepositPct,
|
||||
baseCurrencyCode,
|
||||
counterCurrencyCode,
|
||||
paymentAccount.getPaymentMethod().getId(),
|
||||
@ -203,7 +218,7 @@ public class CreateOfferService {
|
||||
bankId,
|
||||
acceptedBanks,
|
||||
Version.VERSION,
|
||||
xmrWalletService.getWallet().getHeight(),
|
||||
xmrWalletService.getHeight(),
|
||||
maxTradeLimit,
|
||||
maxTradePeriod,
|
||||
useAutoClose,
|
||||
@ -211,44 +226,110 @@ public class CreateOfferService {
|
||||
upperClosePrice,
|
||||
lowerClosePrice,
|
||||
isPrivateOffer,
|
||||
hashOfChallenge,
|
||||
challengeHash,
|
||||
extraDataMap,
|
||||
Version.TRADE_PROTOCOL_VERSION,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
null,
|
||||
extraInfo);
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
offer.setChallenge(challenge);
|
||||
return offer;
|
||||
}
|
||||
|
||||
public BigInteger getReservedFundsForOffer(OfferDirection direction,
|
||||
BigInteger amount,
|
||||
double buyerSecurityDeposit,
|
||||
double sellerSecurityDeposit) {
|
||||
// TODO: add trigger price?
|
||||
public Offer createClonedOffer(Offer sourceOffer,
|
||||
String currencyCode,
|
||||
Price fixedPrice,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
PaymentAccount paymentAccount,
|
||||
String extraInfo) {
|
||||
log.info("Cloning offer with sourceId={}, " +
|
||||
"currencyCode={}, " +
|
||||
"fixedPrice={}, " +
|
||||
"useMarketBasedPrice={}, " +
|
||||
"marketPriceMargin={}, " +
|
||||
"extraInfo={}",
|
||||
sourceOffer.getId(),
|
||||
currencyCode,
|
||||
fixedPrice == null ? null : fixedPrice.getValue(),
|
||||
useMarketBasedPrice,
|
||||
marketPriceMargin,
|
||||
extraInfo);
|
||||
|
||||
BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
|
||||
amount,
|
||||
buyerSecurityDeposit,
|
||||
sellerSecurityDeposit);
|
||||
if (!offerUtil.isBuyOffer(direction))
|
||||
reservedFundsForOffer = reservedFundsForOffer.add(amount);
|
||||
OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
|
||||
String newOfferId = OfferUtil.getRandomOfferId();
|
||||
Offer editedOffer = createAndGetOffer(newOfferId,
|
||||
sourceOfferPayload.getDirection(),
|
||||
currencyCode,
|
||||
BigInteger.valueOf(sourceOfferPayload.getAmount()),
|
||||
BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
|
||||
fixedPrice,
|
||||
useMarketBasedPrice,
|
||||
marketPriceMargin,
|
||||
sourceOfferPayload.getSellerSecurityDepositPct(),
|
||||
paymentAccount,
|
||||
sourceOfferPayload.isPrivateOffer(),
|
||||
sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
|
||||
extraInfo);
|
||||
|
||||
return reservedFundsForOffer;
|
||||
}
|
||||
|
||||
public BigInteger getSecurityDeposit(OfferDirection direction,
|
||||
BigInteger amount,
|
||||
double buyerSecurityDeposit,
|
||||
double sellerSecurityDeposit) {
|
||||
return offerUtil.isBuyOffer(direction) ?
|
||||
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
|
||||
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
|
||||
}
|
||||
|
||||
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
|
||||
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
|
||||
Restrictions.getSellerSecurityDepositAsPercent();
|
||||
// generate one-time challenge for private offer
|
||||
String challenge = null;
|
||||
String challengeHash = null;
|
||||
if (sourceOfferPayload.isPrivateOffer()) {
|
||||
challenge = HavenoUtils.generateChallenge();
|
||||
challengeHash = HavenoUtils.getChallengeHash(challenge);
|
||||
}
|
||||
|
||||
OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
|
||||
long date = new Date().getTime();
|
||||
OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
|
||||
date,
|
||||
sourceOfferPayload.getOwnerNodeAddress(),
|
||||
sourceOfferPayload.getPubKeyRing(),
|
||||
sourceOfferPayload.getDirection(),
|
||||
editedOfferPayload.getPrice(),
|
||||
editedOfferPayload.getMarketPriceMarginPct(),
|
||||
editedOfferPayload.isUseMarketBasedPrice(),
|
||||
sourceOfferPayload.getAmount(),
|
||||
sourceOfferPayload.getMinAmount(),
|
||||
sourceOfferPayload.getMakerFeePct(),
|
||||
sourceOfferPayload.getTakerFeePct(),
|
||||
sourceOfferPayload.getPenaltyFeePct(),
|
||||
sourceOfferPayload.getBuyerSecurityDepositPct(),
|
||||
sourceOfferPayload.getSellerSecurityDepositPct(),
|
||||
editedOfferPayload.getBaseCurrencyCode(),
|
||||
editedOfferPayload.getCounterCurrencyCode(),
|
||||
editedOfferPayload.getPaymentMethodId(),
|
||||
editedOfferPayload.getMakerPaymentAccountId(),
|
||||
editedOfferPayload.getCountryCode(),
|
||||
editedOfferPayload.getAcceptedCountryCodes(),
|
||||
editedOfferPayload.getBankId(),
|
||||
editedOfferPayload.getAcceptedBankIds(),
|
||||
editedOfferPayload.getVersionNr(),
|
||||
sourceOfferPayload.getBlockHeightAtOfferCreation(),
|
||||
editedOfferPayload.getMaxTradeLimit(),
|
||||
editedOfferPayload.getMaxTradePeriod(),
|
||||
sourceOfferPayload.isUseAutoClose(),
|
||||
sourceOfferPayload.isUseReOpenAfterAutoClose(),
|
||||
sourceOfferPayload.getLowerClosePrice(),
|
||||
sourceOfferPayload.getUpperClosePrice(),
|
||||
sourceOfferPayload.isPrivateOffer(),
|
||||
challengeHash,
|
||||
editedOfferPayload.getExtraDataMap(),
|
||||
sourceOfferPayload.getProtocolVersion(),
|
||||
null,
|
||||
null,
|
||||
sourceOfferPayload.getReserveTxKeyImages(),
|
||||
editedOfferPayload.getExtraInfo());
|
||||
Offer clonedOffer = new Offer(clonedOfferPayload);
|
||||
clonedOffer.setPriceFeedService(priceFeedService);
|
||||
clonedOffer.setChallenge(challenge);
|
||||
clonedOffer.setState(Offer.State.AVAILABLE);
|
||||
return clonedOffer;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -259,26 +340,4 @@ public class CreateOfferService {
|
||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
||||
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
|
||||
}
|
||||
|
||||
private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) {
|
||||
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount);
|
||||
return getBoundedBuyerSecurityDeposit(percentOfAmount);
|
||||
}
|
||||
|
||||
private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) {
|
||||
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount);
|
||||
return getBoundedSellerSecurityDeposit(percentOfAmount);
|
||||
}
|
||||
|
||||
private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) {
|
||||
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
|
||||
// MinBuyerSecurityDeposit from Restrictions.
|
||||
return Restrictions.getMinBuyerSecurityDeposit().max(value);
|
||||
}
|
||||
|
||||
private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) {
|
||||
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
|
||||
// MinSellerSecurityDeposit from Restrictions.
|
||||
return Restrictions.getMinSellerSecurityDeposit().max(value);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@
|
||||
package haveno.core.offer;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
@ -81,7 +80,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
AVAILABLE,
|
||||
NOT_AVAILABLE,
|
||||
REMOVED,
|
||||
MAKER_OFFLINE
|
||||
MAKER_OFFLINE,
|
||||
INVALID
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -114,6 +114,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
@Setter
|
||||
transient private boolean isReservedFundsSpent;
|
||||
|
||||
@JsonExclude
|
||||
@Getter
|
||||
@Setter
|
||||
@Nullable
|
||||
transient private String challenge;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -209,23 +215,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
return offerPayload.getPrice();
|
||||
}
|
||||
|
||||
public void verifyTakersTradePrice(long takersTradePrice) throws TradePriceOutOfToleranceException,
|
||||
public void verifyTradePrice(long price) throws TradePriceOutOfToleranceException,
|
||||
MarketPriceNotAvailableException, IllegalArgumentException {
|
||||
if (!isUseMarketBasedPrice()) {
|
||||
checkArgument(takersTradePrice == getFixedPrice(),
|
||||
checkArgument(price == getFixedPrice(),
|
||||
"Takers price does not match offer price. " +
|
||||
"Takers price=" + takersTradePrice + "; offer price=" + getFixedPrice());
|
||||
"Takers price=" + price + "; offer price=" + getFixedPrice());
|
||||
return;
|
||||
}
|
||||
|
||||
Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice);
|
||||
Price tradePrice = Price.valueOf(getCurrencyCode(), price);
|
||||
Price offerPrice = getPrice();
|
||||
if (offerPrice == null)
|
||||
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
|
||||
|
||||
checkArgument(takersTradePrice > 0, "takersTradePrice must be positive");
|
||||
checkArgument(price > 0, "takersTradePrice must be positive");
|
||||
|
||||
double relation = (double) takersTradePrice / (double) offerPrice.getValue();
|
||||
double relation = (double) price / (double) offerPrice.getValue();
|
||||
// We allow max. 2 % difference between own offerPayload price calculation and takers calculation.
|
||||
// Market price might be different at maker's and takers side so we need a bit of tolerance.
|
||||
// The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations
|
||||
@ -233,7 +239,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
|
||||
double deviation = Math.abs(1 - relation);
|
||||
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
|
||||
getShortId(), getCurrencyCode(), takersTradePrice, offerPrice.getValue(),
|
||||
getShortId(), getCurrencyCode(), price, offerPrice.getValue(),
|
||||
deviation * 100 + "%");
|
||||
if (deviation > PRICE_TOLERANCE) {
|
||||
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
|
||||
@ -274,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
UserThread.await(() -> errorMessageProperty.set(errorMessage));
|
||||
errorMessageProperty.set(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
@ -282,12 +288,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
// Getter
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// get the amount needed for the maker to reserve the offer
|
||||
public BigInteger getReserveAmount() {
|
||||
BigInteger reserveAmount = getDirection() == OfferDirection.BUY ? getMaxBuyerSecurityDeposit() : getMaxSellerSecurityDeposit();
|
||||
if (getDirection() == OfferDirection.SELL) reserveAmount = reserveAmount.add(getAmount());
|
||||
reserveAmount = reserveAmount.add(getMaxMakerFee());
|
||||
return reserveAmount;
|
||||
// amount needed for the maker to reserve the offer
|
||||
public BigInteger getAmountNeeded() {
|
||||
BigInteger amountNeeded = getDirection() == OfferDirection.BUY ? getMaxBuyerSecurityDeposit() : getMaxSellerSecurityDeposit();
|
||||
if (getDirection() == OfferDirection.SELL) amountNeeded = amountNeeded.add(getAmount());
|
||||
amountNeeded = amountNeeded.add(getMaxMakerFee());
|
||||
return amountNeeded;
|
||||
}
|
||||
|
||||
// amount reserved for offer
|
||||
public BigInteger getReservedAmount() {
|
||||
if (offerPayload.getReserveTxKeyImages() == null) return null;
|
||||
return HavenoUtils.xmrWalletService.getOutputsAmount(offerPayload.getReserveTxKeyImages());
|
||||
}
|
||||
|
||||
public BigInteger getMaxMakerFee() {
|
||||
@ -330,6 +342,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
return offerPayload.getSellerSecurityDepositPct();
|
||||
}
|
||||
|
||||
public boolean isPrivateOffer() {
|
||||
return offerPayload.isPrivateOffer();
|
||||
}
|
||||
|
||||
public String getChallengeHash() {
|
||||
return offerPayload.getChallengeHash();
|
||||
}
|
||||
|
||||
public boolean hasBuyerAsTakerWithoutDeposit() {
|
||||
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
|
||||
}
|
||||
|
||||
public BigInteger getMaxTradeLimit() {
|
||||
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
|
||||
}
|
||||
@ -396,11 +420,35 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getExtraInfo() {
|
||||
public String getCombinedExtraInfo() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) {
|
||||
sb.append(getOfferExtraInfo());
|
||||
}
|
||||
if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) {
|
||||
if (sb.length() > 0) sb.append("\n\n");
|
||||
sb.append(getPaymentAccountExtraInfo());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getOfferExtraInfo() {
|
||||
return offerPayload.getExtraInfo();
|
||||
}
|
||||
|
||||
public String getPaymentAccountExtraInfo() {
|
||||
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
|
||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO);
|
||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO);
|
||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAYPAL_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
|
||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
|
||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO))
|
||||
return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
@ -36,7 +36,9 @@ package haveno.core.offer;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import common.utils.GenUtils;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.file.JsonFileManager;
|
||||
@ -47,43 +49,50 @@ import haveno.core.filter.FilterManager;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.util.JsonUtil;
|
||||
import haveno.core.xmr.wallet.Restrictions;
|
||||
import haveno.core.xmr.wallet.XmrKeyImageListener;
|
||||
import haveno.core.xmr.wallet.XmrKeyImagePoller;
|
||||
import haveno.network.p2p.BootstrapListener;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import haveno.network.p2p.storage.HashMapChangedListener;
|
||||
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
import haveno.network.utils.Utils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
|
||||
/**
|
||||
* Handles storage and retrieval of offers.
|
||||
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
|
||||
* Handles validation and announcement of offers added or removed.
|
||||
*/
|
||||
@Slf4j
|
||||
public class OfferBookService {
|
||||
|
||||
private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
private final P2PService p2PService;
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
|
||||
private final FilterManager filterManager;
|
||||
private final JsonFileManager jsonFileManager;
|
||||
private final XmrConnectionService xmrConnectionService;
|
||||
|
||||
// poll key images of offers
|
||||
private XmrKeyImagePoller keyImagePoller;
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
||||
private final List<Offer> validOffers = new ArrayList<Offer>();
|
||||
private final List<Offer> invalidOffers = new ArrayList<Offer>();
|
||||
private final Map<String, Timer> invalidOfferTimers = new HashMap<>();
|
||||
|
||||
public interface OfferBookChangedListener {
|
||||
void onAdded(Offer offer);
|
||||
|
||||
void onRemoved(Offer offer);
|
||||
}
|
||||
|
||||
@ -104,58 +113,66 @@ public class OfferBookService {
|
||||
this.xmrConnectionService = xmrConnectionService;
|
||||
jsonFileManager = new JsonFileManager(storageDir);
|
||||
|
||||
// listen for connection changes to monerod
|
||||
xmrConnectionService.addConnectionListener((connection) -> {
|
||||
maybeInitializeKeyImagePoller();
|
||||
keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
|
||||
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
||||
});
|
||||
|
||||
// listen for offers
|
||||
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
||||
@Override
|
||||
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||
ThreadUtils.execute(() -> {
|
||||
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||
synchronized (offerBookChangedListeners) {
|
||||
offerBookChangedListeners.forEach(listener -> {
|
||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||
maybeInitializeKeyImagePoller();
|
||||
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
setReservedFundsSpent(offer);
|
||||
listener.onAdded(offer);
|
||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
synchronized (validOffers) {
|
||||
try {
|
||||
validateOfferPayload(offerPayload);
|
||||
replaceValidOffer(offer);
|
||||
announceOfferAdded(offer);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// ignore illegal offers
|
||||
} catch (RuntimeException e) {
|
||||
replaceInvalidOffer(offer); // offer can become valid later
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, OfferBookService.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||
synchronized (offerBookChangedListeners) {
|
||||
offerBookChangedListeners.forEach(listener -> {
|
||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||
maybeInitializeKeyImagePoller();
|
||||
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
setReservedFundsSpent(offer);
|
||||
listener.onRemoved(offer);
|
||||
ThreadUtils.execute(() -> {
|
||||
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||
removeValidOffer(offerPayload.getId());
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
announceOfferRemoved(offer);
|
||||
|
||||
// check if invalid offers are now valid
|
||||
synchronized (invalidOffers) {
|
||||
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
|
||||
try {
|
||||
validateOfferPayload(invalidOffer.getOfferPayload());
|
||||
removeInvalidOffer(invalidOffer.getId());
|
||||
replaceValidOffer(invalidOffer);
|
||||
announceOfferAdded(invalidOffer);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, OfferBookService.class.getSimpleName());
|
||||
}
|
||||
});
|
||||
|
||||
if (dumpStatistics) {
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
public void onDataReceived() {
|
||||
addOfferBookChangedListener(new OfferBookChangedListener() {
|
||||
@Override
|
||||
public void onAdded(Offer offer) {
|
||||
@ -171,6 +188,16 @@ public class OfferBookService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// listen for changes to key images
|
||||
xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() {
|
||||
@Override
|
||||
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
|
||||
for (String keyImage : spentStatuses.keySet()) {
|
||||
updateAffectedOffers(keyImage);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -178,6 +205,10 @@ public class OfferBookService {
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public boolean hasOffer(String offerId) {
|
||||
return hasValidOffer(offerId);
|
||||
}
|
||||
|
||||
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
if (filterManager.requireUpdateToNewVersionForTrading()) {
|
||||
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
|
||||
@ -233,16 +264,9 @@ public class OfferBookService {
|
||||
}
|
||||
|
||||
public List<Offer> getOffers() {
|
||||
return p2PService.getDataMap().values().stream()
|
||||
.filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload)
|
||||
.map(data -> {
|
||||
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
setReservedFundsSpent(offer);
|
||||
return offer;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
synchronized (validOffers) {
|
||||
return new ArrayList<>(validOffers);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
|
||||
@ -266,7 +290,7 @@ public class OfferBookService {
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
if (keyImagePoller != null) keyImagePoller.clearKeyImages();
|
||||
xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
|
||||
}
|
||||
|
||||
|
||||
@ -274,53 +298,144 @@ public class OfferBookService {
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private synchronized void maybeInitializeKeyImagePoller() {
|
||||
if (keyImagePoller != null) return;
|
||||
keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
|
||||
private void announceOfferAdded(Offer offer) {
|
||||
xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
|
||||
updateReservedFundsSpentStatus(offer);
|
||||
synchronized (offerBookChangedListeners) {
|
||||
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
|
||||
}
|
||||
}
|
||||
|
||||
// handle when key images spent
|
||||
keyImagePoller.addListener(new XmrKeyImageListener() {
|
||||
@Override
|
||||
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
|
||||
for (String keyImage : spentStatuses.keySet()) {
|
||||
updateAffectedOffers(keyImage);
|
||||
}
|
||||
private void announceOfferRemoved(Offer offer) {
|
||||
updateReservedFundsSpentStatus(offer);
|
||||
removeKeyImages(offer);
|
||||
synchronized (offerBookChangedListeners) {
|
||||
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasValidOffer(String offerId) {
|
||||
for (Offer offer : getOffers()) {
|
||||
if (offer.getId().equals(offerId)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// first poll after 20s
|
||||
// TODO: remove?
|
||||
new Thread(() -> {
|
||||
GenUtils.waitFor(20000);
|
||||
keyImagePoller.poll();
|
||||
}).start();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void replaceValidOffer(Offer offer) {
|
||||
synchronized (validOffers) {
|
||||
removeValidOffer(offer.getId());
|
||||
validOffers.add(offer);
|
||||
}
|
||||
}
|
||||
|
||||
private long getKeyImageRefreshPeriodMs() {
|
||||
return xmrConnectionService.isConnectionLocal() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
|
||||
private void replaceInvalidOffer(Offer offer) {
|
||||
synchronized (invalidOffers) {
|
||||
removeInvalidOffer(offer.getId());
|
||||
invalidOffers.add(offer);
|
||||
|
||||
// remove invalid offer after timeout
|
||||
synchronized (invalidOfferTimers) {
|
||||
Timer timer = invalidOfferTimers.get(offer.getId());
|
||||
if (timer != null) timer.stop();
|
||||
timer = UserThread.runAfter(() -> {
|
||||
removeInvalidOffer(offer.getId());
|
||||
}, INVALID_OFFERS_TIMEOUT);
|
||||
invalidOfferTimers.put(offer.getId(), timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeValidOffer(String offerId) {
|
||||
synchronized (validOffers) {
|
||||
validOffers.removeIf(offer -> offer.getId().equals(offerId));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeInvalidOffer(String offerId) {
|
||||
synchronized (invalidOffers) {
|
||||
invalidOffers.removeIf(offer -> offer.getId().equals(offerId));
|
||||
|
||||
// remove timeout
|
||||
synchronized (invalidOfferTimers) {
|
||||
Timer timer = invalidOfferTimers.get(offerId);
|
||||
if (timer != null) timer.stop();
|
||||
invalidOfferTimers.remove(offerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOfferPayload(OfferPayload offerPayload) {
|
||||
|
||||
// validate offer is not banned
|
||||
if (filterManager.isOfferIdBanned(offerPayload.getId())) {
|
||||
throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId());
|
||||
}
|
||||
|
||||
// validate v3 node address compliance
|
||||
boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName());
|
||||
if (!isV3NodeAddressCompliant) {
|
||||
throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId());
|
||||
}
|
||||
|
||||
// validate against existing offers
|
||||
synchronized (validOffers) {
|
||||
int numOffersWithSharedKeyImages = 0;
|
||||
for (Offer offer : validOffers) {
|
||||
|
||||
// validate that no offer has overlapping but different key images
|
||||
if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
|
||||
!Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) {
|
||||
throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId());
|
||||
}
|
||||
|
||||
// validate that no offer has same key images, payment method, and currency
|
||||
if (!offer.getId().equals(offerPayload.getId()) &&
|
||||
offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
|
||||
offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) &&
|
||||
offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) &&
|
||||
offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) {
|
||||
throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId());
|
||||
}
|
||||
|
||||
// count offers with same key images
|
||||
if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1);
|
||||
}
|
||||
|
||||
// validate max offers with same key images
|
||||
if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeKeyImages(Offer offer) {
|
||||
Set<String> unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages());
|
||||
synchronized (validOffers) {
|
||||
for (Offer validOffer : validOffers) {
|
||||
if (validOffer.getId().equals(offer.getId())) continue;
|
||||
unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages());
|
||||
}
|
||||
}
|
||||
xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName());
|
||||
}
|
||||
|
||||
private void updateAffectedOffers(String keyImage) {
|
||||
for (Offer offer : getOffers()) {
|
||||
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
||||
updateReservedFundsSpentStatus(offer);
|
||||
synchronized (offerBookChangedListeners) {
|
||||
offerBookChangedListeners.forEach(listener -> {
|
||||
|
||||
// notify off thread to avoid deadlocking
|
||||
new Thread(() -> {
|
||||
listener.onRemoved(offer);
|
||||
listener.onAdded(offer);
|
||||
}).start();
|
||||
listener.onRemoved(offer);
|
||||
listener.onAdded(offer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setReservedFundsSpent(Offer offer) {
|
||||
if (keyImagePoller == null) return;
|
||||
private void updateReservedFundsSpentStatus(Offer offer) {
|
||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
|
||||
if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
|
||||
offer.setReservedFundsSpent(true);
|
||||
}
|
||||
}
|
||||
|
@ -28,13 +28,10 @@ import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -204,7 +201,7 @@ public class OfferFilterService {
|
||||
accountAgeWitnessService);
|
||||
long myTradeLimit = accountOptional
|
||||
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||
offer.getCurrencyCode(), offer.getMirroredDirection()))
|
||||
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
|
||||
.orElse(0L);
|
||||
long offerMinAmount = offer.getMinAmount().longValueExact();
|
||||
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",
|
||||
@ -218,7 +215,7 @@ public class OfferFilterService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean hasValidSignature(Offer offer) {
|
||||
private boolean hasValidSignature(Offer offer) {
|
||||
|
||||
// get accepted arbitrator by address
|
||||
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
||||
@ -230,9 +227,11 @@ public class OfferFilterService {
|
||||
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
|
||||
} else {
|
||||
|
||||
// otherwise log warning that arbitrator is unregistered
|
||||
List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
||||
log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
|
||||
// // otherwise log warning that arbitrator is unregistered
|
||||
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
||||
// if (!arbitratorAddresses.isEmpty()) {
|
||||
// log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,11 +94,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
// Keys for extra map
|
||||
// Only set for traditional offers
|
||||
public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash";
|
||||
public static final String CASHAPP_EXTRA_INFO = "cashAppExtraInfo";
|
||||
public static final String REFERRAL_ID = "referralId";
|
||||
// Only used in payment method F2F
|
||||
public static final String F2F_CITY = "f2fCity";
|
||||
public static final String F2F_EXTRA_INFO = "f2fExtraInfo";
|
||||
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
|
||||
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
|
||||
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
|
||||
public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo";
|
||||
|
||||
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
|
||||
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
|
||||
@ -153,7 +157,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
|
||||
private final boolean isPrivateOffer;
|
||||
@Nullable
|
||||
private final String hashOfChallenge;
|
||||
private final String challengeHash;
|
||||
@Nullable
|
||||
private final String extraInfo;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -192,12 +198,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
long lowerClosePrice,
|
||||
long upperClosePrice,
|
||||
boolean isPrivateOffer,
|
||||
@Nullable String hashOfChallenge,
|
||||
@Nullable String challengeHash,
|
||||
@Nullable Map<String, String> extraDataMap,
|
||||
int protocolVersion,
|
||||
@Nullable NodeAddress arbitratorSigner,
|
||||
@Nullable byte[] arbitratorSignature,
|
||||
@Nullable List<String> reserveTxKeyImages) {
|
||||
@Nullable List<String> reserveTxKeyImages,
|
||||
@Nullable String extraInfo) {
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.ownerNodeAddress = ownerNodeAddress;
|
||||
@ -235,7 +242,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
this.lowerClosePrice = lowerClosePrice;
|
||||
this.upperClosePrice = upperClosePrice;
|
||||
this.isPrivateOffer = isPrivateOffer;
|
||||
this.hashOfChallenge = hashOfChallenge;
|
||||
this.challengeHash = challengeHash;
|
||||
this.extraInfo = extraInfo;
|
||||
}
|
||||
|
||||
public byte[] getHash() {
|
||||
@ -281,12 +289,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
lowerClosePrice,
|
||||
upperClosePrice,
|
||||
isPrivateOffer,
|
||||
hashOfChallenge,
|
||||
challengeHash,
|
||||
extraDataMap,
|
||||
protocolVersion,
|
||||
arbitratorSigner,
|
||||
null,
|
||||
reserveTxKeyImages
|
||||
reserveTxKeyImages,
|
||||
null
|
||||
);
|
||||
|
||||
return signee.getHash();
|
||||
@ -325,12 +334,21 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
|
||||
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
||||
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
|
||||
return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted);
|
||||
boolean isBuyerTaker = getDirection() == OfferDirection.SELL;
|
||||
if (isPrivateOffer() && isBuyerTaker) {
|
||||
return securityDepositUnadjusted;
|
||||
} else {
|
||||
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
|
||||
}
|
||||
}
|
||||
|
||||
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
||||
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
|
||||
return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted);
|
||||
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
|
||||
}
|
||||
|
||||
public boolean isBuyerAsTakerWithoutDeposit() {
|
||||
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -373,11 +391,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
|
||||
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
|
||||
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
|
||||
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
|
||||
Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash);
|
||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
|
||||
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
|
||||
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
|
||||
|
||||
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
||||
}
|
||||
@ -387,7 +406,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
null : new ArrayList<>(proto.getAcceptedBankIdsList());
|
||||
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
|
||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
||||
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
||||
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||
null : proto.getExtraDataMap();
|
||||
|
||||
@ -423,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
proto.getLowerClosePrice(),
|
||||
proto.getUpperClosePrice(),
|
||||
proto.getIsPrivateOffer(),
|
||||
hashOfChallenge,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
|
||||
extraDataMapMap,
|
||||
proto.getProtocolVersion(),
|
||||
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
||||
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
|
||||
reserveTxKeyImages,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -470,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
",\r\n lowerClosePrice=" + lowerClosePrice +
|
||||
",\r\n upperClosePrice=" + upperClosePrice +
|
||||
",\r\n isPrivateOffer=" + isPrivateOffer +
|
||||
",\r\n hashOfChallenge='" + hashOfChallenge + '\'' +
|
||||
",\r\n challengeHash='" + challengeHash +
|
||||
",\r\n arbitratorSigner=" + arbitratorSigner +
|
||||
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
||||
",\r\n extraInfo='" + extraInfo +
|
||||
"\r\n} ";
|
||||
}
|
||||
|
||||
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
|
||||
// The json is used for the hash in the contract and change of oder would cause a different hash and
|
||||
// The json is used for the hash in the contract and change of order would cause a different hash and
|
||||
// therefore a failure during trade.
|
||||
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
|
||||
@Override
|
||||
@ -514,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
|
||||
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
|
||||
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
|
||||
object.add("extraInfo", context.serialize(offerPayload.getExtraInfo()));
|
||||
// reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
@ -35,15 +35,24 @@ import haveno.core.monetary.Price;
|
||||
import haveno.core.monetary.TraditionalMoney;
|
||||
import haveno.core.monetary.Volume;
|
||||
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
|
||||
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.CAPABILITIES;
|
||||
import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.F2F_CITY;
|
||||
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.PAY_BY_MAIL_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.PAYPAL_EXTRA_INFO;
|
||||
import static haveno.core.offer.OfferPayload.REFERRAL_ID;
|
||||
import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF;
|
||||
import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
|
||||
|
||||
import haveno.core.payment.AustraliaPayidAccount;
|
||||
import haveno.core.payment.CashAppAccount;
|
||||
import haveno.core.payment.CashAtAtmAccount;
|
||||
import haveno.core.payment.F2FAccount;
|
||||
import haveno.core.payment.PayByMailAccount;
|
||||
import haveno.core.payment.PayPalAccount;
|
||||
import haveno.core.payment.PaymentAccount;
|
||||
import haveno.core.provider.price.MarketPrice;
|
||||
import haveno.core.provider.price.PriceFeedService;
|
||||
@ -51,8 +60,8 @@ import haveno.core.trade.statistics.ReferralIdService;
|
||||
import haveno.core.user.AutoConfirmSettings;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
@ -113,9 +122,10 @@ public class OfferUtil {
|
||||
|
||||
public long getMaxTradeLimit(PaymentAccount paymentAccount,
|
||||
String currencyCode,
|
||||
OfferDirection direction) {
|
||||
OfferDirection direction,
|
||||
boolean buyerAsTakerWithoutDeposit) {
|
||||
return paymentAccount != null
|
||||
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
|
||||
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
|
||||
: 0;
|
||||
}
|
||||
|
||||
@ -197,6 +207,22 @@ public class OfferUtil {
|
||||
extraDataMap.put(PAY_BY_MAIL_EXTRA_INFO, ((PayByMailAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
if (paymentAccount instanceof PayPalAccount) {
|
||||
extraDataMap.put(PAYPAL_EXTRA_INFO, ((PayPalAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
if (paymentAccount instanceof CashAppAccount) {
|
||||
extraDataMap.put(CASHAPP_EXTRA_INFO, ((CashAppAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
if (paymentAccount instanceof AustraliaPayidAccount) {
|
||||
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
if (paymentAccount instanceof CashAtAtmAccount) {
|
||||
extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
|
||||
|
||||
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
|
||||
@ -209,16 +235,16 @@ public class OfferUtil {
|
||||
return extraDataMap.isEmpty() ? null : extraDataMap;
|
||||
}
|
||||
|
||||
public void validateOfferData(double buyerSecurityDeposit,
|
||||
public void validateOfferData(double securityDeposit,
|
||||
PaymentAccount paymentAccount,
|
||||
String currencyCode) {
|
||||
checkNotNull(p2PService.getAddress(), "Address must not be null");
|
||||
checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
|
||||
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
|
||||
"securityDeposit must not exceed " +
|
||||
getMaxBuyerSecurityDepositAsPercent());
|
||||
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
|
||||
getMaxSecurityDepositAsPercent());
|
||||
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
|
||||
"securityDeposit must not be less than " +
|
||||
getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
|
||||
getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
|
||||
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
|
||||
Res.get("offerbook.warning.currencyBanned"));
|
||||
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
|
||||
|
@ -34,8 +34,6 @@
|
||||
|
||||
package haveno.core.offer;
|
||||
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.proto.ProtoUtil;
|
||||
import haveno.core.trade.Tradable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@ -44,23 +42,19 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@Slf4j
|
||||
public final class OpenOffer implements Tradable {
|
||||
// Timeout for offer reservation during takeoffer process. If deposit tx is not completed in that time we reset the offer to AVAILABLE state.
|
||||
private static final long TIMEOUT = 60;
|
||||
transient private Timer timeoutTimer;
|
||||
|
||||
public enum State {
|
||||
SCHEDULED,
|
||||
PENDING,
|
||||
AVAILABLE,
|
||||
RESERVED,
|
||||
CLOSED,
|
||||
@ -103,11 +97,26 @@ public final class OpenOffer implements Tradable {
|
||||
@Getter
|
||||
private String reserveTxKey;
|
||||
@Getter
|
||||
@Setter
|
||||
private String challenge;
|
||||
@Getter
|
||||
private final long triggerPrice;
|
||||
@Getter
|
||||
@Setter
|
||||
transient private long mempoolStatus = -1;
|
||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||
@Getter
|
||||
@Setter
|
||||
transient boolean isProcessing = false;
|
||||
@Getter
|
||||
@Setter
|
||||
transient int numProcessingAttempts = 0;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean deactivatedByTrigger;
|
||||
@Getter
|
||||
@Setter
|
||||
private String groupId;
|
||||
|
||||
public OpenOffer(Offer offer) {
|
||||
this(offer, 0, false);
|
||||
@ -121,7 +130,9 @@ public final class OpenOffer implements Tradable {
|
||||
this.offer = offer;
|
||||
this.triggerPrice = triggerPrice;
|
||||
this.reserveExactAmount = reserveExactAmount;
|
||||
state = State.SCHEDULED;
|
||||
this.challenge = offer.getChallenge();
|
||||
this.groupId = UUID.randomUUID().toString();
|
||||
state = State.PENDING;
|
||||
}
|
||||
|
||||
public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) {
|
||||
@ -138,6 +149,9 @@ public final class OpenOffer implements Tradable {
|
||||
this.reserveTxHash = openOffer.reserveTxHash;
|
||||
this.reserveTxHex = openOffer.reserveTxHex;
|
||||
this.reserveTxKey = openOffer.reserveTxKey;
|
||||
this.challenge = openOffer.challenge;
|
||||
this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
|
||||
this.groupId = openOffer.groupId;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -154,7 +168,10 @@ public final class OpenOffer implements Tradable {
|
||||
long splitOutputTxFee,
|
||||
@Nullable String reserveTxHash,
|
||||
@Nullable String reserveTxHex,
|
||||
@Nullable String reserveTxKey) {
|
||||
@Nullable String reserveTxKey,
|
||||
@Nullable String challenge,
|
||||
boolean deactivatedByTrigger,
|
||||
@Nullable String groupId) {
|
||||
this.offer = offer;
|
||||
this.state = state;
|
||||
this.triggerPrice = triggerPrice;
|
||||
@ -165,9 +182,13 @@ public final class OpenOffer implements Tradable {
|
||||
this.reserveTxHash = reserveTxHash;
|
||||
this.reserveTxHex = reserveTxHex;
|
||||
this.reserveTxKey = reserveTxKey;
|
||||
this.challenge = challenge;
|
||||
this.deactivatedByTrigger = deactivatedByTrigger;
|
||||
if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19)
|
||||
this.groupId = groupId;
|
||||
|
||||
if (this.state == State.RESERVED)
|
||||
setState(State.AVAILABLE);
|
||||
// reset reserved state to available
|
||||
if (this.state == State.RESERVED) setState(State.AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -177,7 +198,8 @@ public final class OpenOffer implements Tradable {
|
||||
.setTriggerPrice(triggerPrice)
|
||||
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
|
||||
.setSplitOutputTxFee(splitOutputTxFee)
|
||||
.setReserveExactAmount(reserveExactAmount);
|
||||
.setReserveExactAmount(reserveExactAmount)
|
||||
.setDeactivatedByTrigger(deactivatedByTrigger);
|
||||
|
||||
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
|
||||
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
|
||||
@ -185,6 +207,8 @@ public final class OpenOffer implements Tradable {
|
||||
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
|
||||
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
|
||||
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
|
||||
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
|
||||
Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId));
|
||||
|
||||
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
|
||||
}
|
||||
@ -198,9 +222,12 @@ public final class OpenOffer implements Tradable {
|
||||
proto.getScheduledTxHashesList(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getSplitOutputTxHash()),
|
||||
proto.getSplitOutputTxFee(),
|
||||
proto.getReserveTxHash(),
|
||||
proto.getReserveTxHex(),
|
||||
proto.getReserveTxKey());
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
|
||||
proto.getDeactivatedByTrigger(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
|
||||
return openOffer;
|
||||
}
|
||||
|
||||
@ -227,21 +254,22 @@ public final class OpenOffer implements Tradable {
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
stateProperty.set(state);
|
||||
|
||||
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
|
||||
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
|
||||
startTimeout();
|
||||
} else {
|
||||
stopTimeout();
|
||||
if (state == State.AVAILABLE) {
|
||||
deactivatedByTrigger = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void deactivate(boolean deactivatedByTrigger) {
|
||||
this.deactivatedByTrigger = deactivatedByTrigger;
|
||||
setState(State.DEACTIVATED);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return stateProperty;
|
||||
}
|
||||
|
||||
public boolean isScheduled() {
|
||||
return state == State.SCHEDULED;
|
||||
public boolean isPending() {
|
||||
return state == State.PENDING;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
@ -252,26 +280,10 @@ public final class OpenOffer implements Tradable {
|
||||
return state == State.DEACTIVATED;
|
||||
}
|
||||
|
||||
private void startTimeout() {
|
||||
stopTimeout();
|
||||
|
||||
timeoutTimer = UserThread.runAfter(() -> {
|
||||
log.debug("Timeout for resetting State.RESERVED reached");
|
||||
if (state == State.RESERVED) {
|
||||
// we do not need to persist that as at startup any RESERVED state would be reset to AVAILABLE anyway
|
||||
setState(State.AVAILABLE);
|
||||
}
|
||||
}, TIMEOUT);
|
||||
public boolean isCanceled() {
|
||||
return state == State.CANCELED;
|
||||
}
|
||||
|
||||
private void stopTimeout() {
|
||||
if (timeoutTimer != null) {
|
||||
timeoutTimer.stop();
|
||||
timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OpenOffer{" +
|
||||
@ -281,6 +293,7 @@ public final class OpenOffer implements Tradable {
|
||||
",\n reserveExactAmount=" + reserveExactAmount +
|
||||
",\n scheduledAmount=" + scheduledAmount +
|
||||
",\n splitOutputTxFee=" + splitOutputTxFee +
|
||||
",\n groupId=" + groupId +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -47,10 +47,12 @@ public final class SignedOfferList extends PersistableListAsObservable<SignedOff
|
||||
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
return protobuf.PersistableEnvelope.newBuilder()
|
||||
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
|
||||
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
|
||||
.build();
|
||||
synchronized (getList()) {
|
||||
return protobuf.PersistableEnvelope.newBuilder()
|
||||
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
|
||||
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static SignedOfferList fromProto(protobuf.SignedOfferList proto) {
|
||||
|
@ -62,7 +62,7 @@ public class TriggerPriceService {
|
||||
} else {
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
public void onDataReceived() {
|
||||
onBootstrapComplete();
|
||||
}
|
||||
});
|
||||
@ -92,12 +92,11 @@ public class TriggerPriceService {
|
||||
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
|
||||
.forEach(marketPrice -> {
|
||||
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
|
||||
.filter(openOffer -> !openOffer.isDeactivated())
|
||||
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||
public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||
Price price = openOffer.getOffer().getPrice();
|
||||
if (price == null || marketPrice == null) {
|
||||
return false;
|
||||
@ -125,13 +124,12 @@ public class TriggerPriceService {
|
||||
}
|
||||
|
||||
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||
if (wasTriggered(marketPrice, openOffer)) {
|
||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
||||
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
|
||||
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
||||
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
||||
long triggerPrice = openOffer.getTriggerPrice();
|
||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
||||
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
|
||||
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
||||
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
||||
|
||||
if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
|
||||
log.info("Market price exceeded the trigger price of the open offer.\n" +
|
||||
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
|
||||
"Market price: {};\nTrigger price: {}",
|
||||
@ -139,14 +137,26 @@ public class TriggerPriceService {
|
||||
currencyCode,
|
||||
openOffer.getOffer().getDirection(),
|
||||
marketPrice.getPrice(),
|
||||
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
|
||||
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
|
||||
);
|
||||
|
||||
openOfferManager.deactivateOpenOffer(openOffer, () -> {
|
||||
openOfferManager.deactivateOpenOffer(openOffer, true, () -> {
|
||||
}, errorMessage -> {
|
||||
});
|
||||
} else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) {
|
||||
log.info("Market price is back within the trigger price of the open offer.\n" +
|
||||
"We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
|
||||
"Market price: {};\nTrigger price: {}",
|
||||
openOffer.getOffer().getShortId(),
|
||||
currencyCode,
|
||||
openOffer.getOffer().getDirection(),
|
||||
marketPrice.getPrice(),
|
||||
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
|
||||
);
|
||||
|
||||
openOfferManager.activateOpenOffer(openOffer, () -> {
|
||||
}, errorMessage -> {
|
||||
});
|
||||
} else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
|
||||
// TODO: check if open offer's reserve tx is failed or double spend seen
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ import haveno.core.offer.AvailabilityResult;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.availability.OfferAvailabilityModel;
|
||||
import haveno.core.offer.messages.OfferAvailabilityResponse;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
@ -52,13 +51,6 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
|
||||
return;
|
||||
}
|
||||
|
||||
// verify maker signature for trade request
|
||||
if (!HavenoUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
|
||||
offer.setState(Offer.State.NOT_AVAILABLE);
|
||||
failed("Take offer attempt failed because maker signature is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
offer.setState(Offer.State.AVAILABLE);
|
||||
model.setMakerSignature(offerAvailabilityResponse.getMakerSignature());
|
||||
checkNotNull(model.getMakerSignature());
|
||||
|
@ -26,6 +26,7 @@ import haveno.core.offer.availability.OfferAvailabilityModel;
|
||||
import haveno.core.offer.messages.OfferAvailabilityRequest;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.messages.InitTradeRequest;
|
||||
import haveno.core.trade.messages.TradeProtocolVersion;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
@ -53,8 +54,9 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||
User user = model.getUser();
|
||||
P2PService p2PService = model.getP2PService();
|
||||
XmrWalletService walletService = model.getXmrWalletService();
|
||||
String paymentAccountId = model.getPaymentAccountId();
|
||||
String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
|
||||
String makerPaymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId();
|
||||
String takerPaymentAccountId = model.getPaymentAccountId();
|
||||
String paymentMethodId = user.getPaymentAccount(takerPaymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
|
||||
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
|
||||
// taker signs offer using offer id as nonce to avoid challenge protocol
|
||||
@ -66,14 +68,16 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||
|
||||
// send InitTradeRequest to maker to sign
|
||||
InitTradeRequest tradeRequest = new InitTradeRequest(
|
||||
TradeProtocolVersion.MULTISIG_2_3, // TODO: replace with first of their accepted protocols
|
||||
offer.getId(),
|
||||
P2PService.getMyNodeAddress(),
|
||||
p2PService.getKeyRing().getPubKeyRing(),
|
||||
model.getTradeAmount().longValueExact(),
|
||||
price.getValue(),
|
||||
user.getAccountId(),
|
||||
paymentAccountId,
|
||||
paymentMethodId,
|
||||
null,
|
||||
user.getAccountId(),
|
||||
makerPaymentAccountId,
|
||||
takerPaymentAccountId,
|
||||
p2PService.getKeyRing().getPubKeyRing(),
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
sig,
|
||||
@ -85,7 +89,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||
null,
|
||||
null,
|
||||
payoutAddress,
|
||||
null);
|
||||
null); // challenge is required when offer taken
|
||||
|
||||
// save trade request to later send to arbitrator
|
||||
model.setTradeRequest(tradeRequest);
|
||||
|
@ -23,6 +23,7 @@ import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
import haveno.core.filter.FilterManager;
|
||||
import haveno.core.offer.OfferBookService;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
import haveno.core.offer.messages.SignOfferResponse;
|
||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import haveno.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
@ -35,7 +36,6 @@ import haveno.network.p2p.P2PService;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@ -61,6 +61,8 @@ public class PlaceOfferModel implements Model {
|
||||
private final FilterManager filterManager;
|
||||
@Getter
|
||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
@Getter
|
||||
private final OpenOfferManager openOfferManager;
|
||||
|
||||
// Mutable
|
||||
@Setter
|
||||
@ -68,8 +70,6 @@ public class PlaceOfferModel implements Model {
|
||||
@Setter
|
||||
private Transaction transaction;
|
||||
@Setter
|
||||
private MoneroTxWallet reserveTx;
|
||||
@Setter
|
||||
private SignOfferResponse signOfferResponse;
|
||||
@Setter
|
||||
@Getter
|
||||
@ -89,7 +89,8 @@ public class PlaceOfferModel implements Model {
|
||||
User user,
|
||||
KeyRing keyRing,
|
||||
FilterManager filterManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService) {
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
OpenOfferManager openOfferManager) {
|
||||
this.openOffer = openOffer;
|
||||
this.reservedFundsForOffer = reservedFundsForOffer;
|
||||
this.useSavingsWallet = useSavingsWallet;
|
||||
@ -105,6 +106,7 @@ public class PlaceOfferModel implements Model {
|
||||
this.keyRing = keyRing;
|
||||
this.filterManager = filterManager;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.openOfferManager = openOfferManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user