Compare commits

...

650 Commits

Author SHA1 Message Date
woodser
13e13d945d print pub key hex when missing for signature data 2025-04-18 17:30:49 -04:00
woodser
bfef0f9492 fix hanging while posting or canceling offer 2025-04-18 17:30:49 -04:00
woodser
39909e7936
bump version to 1.1.0 2025-04-17 20:55:01 -04:00
woodser
695f2b8dd3
fix startup error with localhost, support fallback from provided nodes 2025-04-17 19:58:12 -04:00
woodser
8f778be4d9
show scrollbar as needed when creating offer 2025-04-17 16:40:25 -04:00
woodser
821ef16d8f
refresh polling when key images added 2025-04-17 16:15:26 -04:00
woodser
58590d60df add arbitrator2 mainnet config to Makefile 2025-04-17 14:53:11 -04:00
woodser
8eccbcce43 skip offer signature validation for cloned offer until signed 2025-04-17 14:53:11 -04:00
woodser
bbfc5d5fed remove max version verification so arbitrators can be behind 2025-04-17 14:53:11 -04:00
woodser
22db354cb2 do not re-filter offers for every offer book change 2025-04-17 14:53:11 -04:00
woodser
60ceff6695 fix redundant key image notifications 2025-04-17 14:53:11 -04:00
woodser
c87b8a5b45
add note to keep arbitrator key in the repo to preserve signed accounts 2025-04-14 21:14:46 -04:00
woodser
bf055556f1
fix offers being deleted after minimum version update 2025-04-14 20:45:15 -04:00
woodser
13dc34a805
schedule to import multisig hex after 5 confirmations 2025-04-14 09:27:58 -04:00
woodser
53b0f203de
init main wallet with connection applied in same thread 2025-04-14 08:26:28 -04:00
woodser
52f0c20c8c
do not switch xmr node preference with fixed connection 2025-04-13 19:49:43 -04:00
woodser
a30b41de4b
fix deadlock by setting offer error message property on calling thread 2025-04-13 15:17:13 -04:00
woodser
f1c09161f4
add lock symbol when confirming private offers 2025-04-13 12:03:04 -04:00
woodser
57c2408d07 fix npe sorting open offers by group id 2025-04-13 08:34:23 -04:00
woodser
f5515caad5 improve responsiveness of extra info input by reinvoking main thread 2025-04-13 08:34:23 -04:00
woodser
71b23e0ed9 remove unused code from core trades service 2025-04-13 08:34:23 -04:00
woodser
9a5d2d5862 reset offer protocol after first result 2025-04-13 08:34:23 -04:00
woodser
31782e5255 do not cancel open offer when funds spent and reserved for trade 2025-04-13 08:34:23 -04:00
woodser
96eab3d42f do not fix reserved outputs after shut down started 2025-04-13 08:34:23 -04:00
woodser
454fc91298 fix startup when missing multisig wallets 2025-04-13 08:34:23 -04:00
woodser
5bff265cca take offer runs on trade thread 2025-04-13 08:34:23 -04:00
woodser
295c91760c place offer runs off main thread 2025-04-13 08:34:23 -04:00
woodser
f19ed19325 remove log highlighting with character literal 2025-04-10 10:21:25 -04:00
woodser
31fbf9c4e8 ignore fault on mailbox message task after shut down 2025-04-09 10:58:07 -04:00
woodser
765a32fd9f improve error message when open offer is removed while initializing trade 2025-04-09 10:58:07 -04:00
woodser
fe1fb88ce0 import multisig hex on trade thread when scheduled 2025-04-09 10:58:07 -04:00
woodser
35eb65d173 clear monero connection error & popup on successful poll 2025-04-09 10:58:07 -04:00
woodser
974c6a0d86 shut down p2p service last to fix timeout on shut down 2025-04-09 10:58:07 -04:00
woodser
ad38e3b80c remove trade lock while shutting down trade thread 2025-04-09 10:58:07 -04:00
woodser
7243d7fa38
show user friendly error on non-ascii password 2025-04-09 08:15:14 -04:00
woodser
d78709e1f9
start trade period from unlock time instead of first confirmation 2025-04-08 09:16:17 -04:00
woodser
34b55bc86b
precede version tags with 'v' for release hash files 2025-04-07 18:40:56 -04:00
boldsuck
501485ec71
Add release to build workflow (#1685) 2025-04-07 15:57:37 -04:00
woodser
1c92d96651
fix offer publishing with mutable list 2025-04-07 15:51:41 -04:00
woodser
08b0b36436 do not open or create wallet after shut down started 2025-04-07 09:27:39 -04:00
woodser
9027ce6634 fix concurrency issues by synchronizing on base persistable list 2025-04-07 09:27:39 -04:00
woodser
3c6914ac7e
filter boxes search currencies, payment details, and extra info 2025-04-06 15:26:09 -04:00
woodser
055b7d1376
fix link to clone offer documentation 2025-04-05 18:44:27 -04:00
woodser
f87dc3a4d1
update information popup for cloned offers 2025-04-05 17:59:34 -04:00
woodser
40e18890d6
support cloning up to 10 offers with shared reserved funds (#1668) 2025-04-05 17:29:55 -04:00
woodser
7e3a47de4a
prompt to start local node or fallback on startup 2025-04-05 17:29:31 -04:00
woodser
9668dd2369
populate trigger price and extra info on duplicate or edit offer 2025-04-04 13:55:25 -04:00
woodser
9bd4f70d02
do not process payment sent & received msgs until deposits confirmed 2025-04-03 18:35:08 -04:00
woodser
39c75cd71b
update withdraw confirmation wording with translations 2025-04-03 18:26:14 -04:00
woodser
8981740b8c
update to monero-project v0.18.4.0 2025-04-03 10:20:18 -04:00
woodser
584cc3b6d4
replace issues hyperlink from bisq to haveno 2025-04-02 17:09:37 -04:00
woodser
dc43e1c329 re-sign offers on edit if applicable 2025-04-02 10:43:29 -04:00
woodser
93369c4211
prompt to fallback if last synced local node is offline on startup 2025-03-28 14:51:19 -04:00
woodser
a21971429c log standby mode as warning 2025-03-28 14:32:58 -04:00
woodser
df9daf99bf change color of highlighted logs to cyan 2025-03-28 14:32:58 -04:00
woodser
e699b427e2 apply log highlighter when running daemon 2025-03-28 14:32:58 -04:00
woodser
29e6540234
fix missing edit icon on offers view 2025-03-26 09:42:00 -04:00
Brandon Trussell
207ff5416c
Support linux aarch64 (#1665) 2025-03-25 06:49:26 -04:00
boldsuck
ad809ff20e
Add price node (#1661) 2025-03-23 08:00:26 -04:00
woodser
b3174518d9
add vscode to IDE documentation 2025-03-22 08:34:50 -04:00
boldsuck
82728aef69
Change http links to https in LICENSE (#1660) 2025-03-22 07:58:05 -04:00
woodser
1a2dcfc704
limit offer extra info to 1500 characters 2025-03-22 07:57:31 -04:00
woodser
ce27818f43 recover payment sent message state on startup if undefined 2025-03-21 10:06:54 -04:00
woodser
a7d8e4560f enable resending payment received msgs based on offer protocol version 2025-03-21 10:06:54 -04:00
woodser
028ced7021 bump offer protocol version, do not verify miner fee for outdated trades 2025-03-21 10:06:54 -04:00
woodser
c95a26e043 re-sign offers on protocol version update 2025-03-21 10:06:54 -04:00
woodser
b5f9bc307b verify offer versions when signing 2025-03-21 10:06:54 -04:00
woodser
bee86daff3 increase grpc rate limit for offers and trades on testnet 2025-03-21 10:06:54 -04:00
woodser
26e3a153bc process messages until trade is completely finished 2025-03-21 10:06:54 -04:00
woodser
2af9019db0 do not reset payment states if payout published 2025-03-21 10:06:54 -04:00
woodser
5107c6ba57 fixes to process payout tx and revert to payment sent state 2025-03-21 10:06:54 -04:00
woodser
79aa214f22 check failed state to determine if payment sent or received 2025-03-21 10:06:54 -04:00
woodser
6f3ae49b68 do not process payment confirmation messages if shut down started 2025-03-21 10:06:54 -04:00
woodser
5711aabad8 remove outdated code for v1.0.7 update 2025-03-21 10:06:54 -04:00
woodser
d7be2885bd
fix error fetching prices with --socks5ProxyXmrAddress config (#1658) 2025-03-20 08:00:23 -04:00
woodser
51fc4d0c41 do not export multisig info on dispute opened 2025-03-18 10:07:40 -04:00
woodser
1b31dc24b8 share dispute opener's updated multisig info on dispute opened 2025-03-18 10:07:40 -04:00
woodser
b19724e33d fix summary info not populated on normal payout after dispute 2025-03-18 10:07:40 -04:00
woodser
07fa0b35e4 fix error message if arbitrator fails to publish deposit txs 2025-03-18 10:07:40 -04:00
woodser
f4f53630d5 automatically cancel offers with duplicate key images 2025-03-18 10:07:40 -04:00
woodser
cb25a23779 refactor message resending, reprocessing, and ack handling 2025-03-18 10:07:40 -04:00
woodser
b7c9dea518
fix links in whonix instructions 2025-03-16 10:21:58 -04:00
PromptPunksFauxCough
34458cf3df
Install Haveno on Whonix + Qubes (#1628) 2025-03-16 10:11:17 -04:00
woodser
63917fe8cc
replace sys.outs with log.info in buyer/seller protocols 2025-03-11 13:42:10 -04:00
woodser
d4eb30bb97 schedule import multisig hex on deposit confirmation msg 2025-03-10 10:53:35 -04:00
woodser
cb69d06468 increase grpc rate limits for testnet 2025-03-10 10:53:35 -04:00
woodser
46734459d4 highlight logs for handling trade protocol messages 2025-03-10 10:53:35 -04:00
woodser
a55daf803e call trade message handling off trade thread 2025-03-10 10:53:35 -04:00
woodser
1510e6f18d logging cleanup 2025-03-10 10:53:35 -04:00
woodser
fb2b4a0c6a save and reprocess payment sent message 2025-03-10 10:53:35 -04:00
woodser
38c0855728 save payment received message immediately for reprocessing 2025-03-10 10:53:35 -04:00
woodser
84d8a17ab4 rename payment sent message state property for seller 2025-03-10 10:53:35 -04:00
woodser
00a2a7c2b7 nack offer availability request if disconnected from xmr node 2025-03-10 08:37:15 -04:00
woodser
251a973fd6 do not refresh or republish offers if disconnected from xmr node 2025-03-10 08:37:15 -04:00
woodser
bedd38748e sign and post offer directly if reserve amount = available balance 2025-03-10 08:30:30 -04:00
woodser
b0e9627c10 rename openOfferManager.getOpenOffer(id) 2025-03-10 08:30:30 -04:00
woodser
bf97fbc7ea skip reset address entries when failed trade is scheduled for deletion 2025-03-10 08:30:06 -04:00
woodser
8b1d2aa203 fix bug to delete scheduled failed trade after restart 2025-03-10 08:30:06 -04:00
woodser
2d46b2ab7c log warning on error taking offer from ui 2025-03-10 08:30:06 -04:00
woodser
9acd7ad584
rename config handler from btc to xmr 2025-03-09 17:02:15 -04:00
woodser
c853c4ffcb
bump version to 1.0.19 2025-03-09 16:54:28 -04:00
boldsuck
e5f729d12f
Update Tor Browser version: 14.0.7 and tor binary version: 0.4.8.14 (#1650) 2025-03-09 14:32:32 -04:00
woodser
03a1132c2f copy monero payment uri to clipboard in qr code window 2025-03-09 09:45:39 -04:00
boldsuck
61a62a1d94
Update tor-upgrade.md docu (#1645) 2025-03-08 19:42:53 -05:00
woodser
a53026be8a
cleanup external tor docs 2025-03-07 07:45:17 -05:00
PromptPunksFauxCough
6b567b94f2
Document external tor usage (#1627) 2025-03-07 07:30:58 -05:00
woodser
c9350e123e
fix npe with xmrNodes with onion address 2025-03-07 07:12:02 -05:00
woodser
68f7067125
fix translation to wait for confirmations when deposits published 2025-03-07 07:11:49 -05:00
woodser
d67d259b2c
update stagenet faucet link 2025-03-07 07:06:03 -05:00
woodser
e24b1c2461
remove rino and melo.tools from public xmr nodes 2025-03-06 11:40:49 -05:00
woodser
67d0589e7b
fix hyperlinks to add new API functions (#1641) 2025-03-06 11:11:10 -05:00
U65535F
060d9fa4f1
Serialize lists to comma delimited string in PaymentAccount.toJson() (#1620) 2025-03-06 07:12:22 -05:00
woodser
52bf1edf79
Revert "direct bind tor node uses configured socks5 proxy" (#1635)
This reverts commit fc42f6314eb593b129ff0bb028c585ce677e0e6d.
2025-03-05 11:19:41 -05:00
woodser
580e5b672c
add lock to submit tx to pool for verification and sync on shut down 2025-03-05 08:39:31 -05:00
woodser
5720ee74b0
run trade charts view listeners on user thread to fix npe 2025-03-05 08:39:03 -05:00
woodser
fff0fa0186
add short name for paysafe 2025-03-05 08:20:24 -05:00
woodser
31b0edca22
do not ignore local node if configured 2025-03-02 10:44:38 -05:00
woodser
48501a6572
direct bind tor node uses configured socks5 proxy 2025-02-28 12:06:46 -05:00
woodser
998b893cc3 update pgp public key for commit verification 2025-02-28 09:33:40 -05:00
woodser
816d273956 add demo video to readme, from @Minecon724 in #1352
Co-authored-by: Minecon724
2025-02-27 14:01:22 -05:00
woodser
28d2bc891f update install instructions for MSYS2 2025-02-27 10:17:10 -05:00
boldsuck
964c71ed1b
Fix broken 'create-mainnet link' 2025-02-26 13:26:47 -05:00
woodser
40924a6f7b prevent wallet backup on windows due to file lock 2025-02-24 08:39:09 -05:00
woodser
8a01a07ac2
update link to ui poc in developer guide 2025-02-22 09:58:23 -05:00
jermanuts
5d457d62c5 fix support link 2025-02-22 08:55:45 -05:00
woodser
b9381f7f9f increase max connections per ip 2025-02-19 07:17:38 -05:00
woodser
4d765fa5d9 resend deposit confirmed and payment sent messages more often until ack 2025-02-18 08:17:33 -05:00
woodser
e4fa5f520d synchronize access to get closed trade by id 2025-02-18 08:11:10 -05:00
woodser
c3b7289943 update chat message ack state on main thread 2025-02-18 08:11:10 -05:00
woodser
024e59a982 support DAI Stablecoin (ERC20) 2025-02-16 16:53:35 -05:00
woodser
667f0c8fb5 rename currency code base util 2025-02-16 09:10:43 -05:00
woodser
0cba254193 add usdt-trc20 as main crypto currency 2025-02-15 08:00:25 -05:00
woodser
f675588a2d allow offer trigger price outside of current price 2025-02-15 07:03:06 -05:00
woodser
290a3738b7 re-enable triggered offers if within trigger price again 2025-02-15 07:03:06 -05:00
woodser
4a82c69507 use default priority for trade transactions 2025-02-13 10:36:10 -05:00
woodser
b72159fcf8 synchronize access to pending trades data model
Co-authored-by: XMRZombie <monerozombie@proton.me>
2025-02-13 10:33:51 -05:00
woodser
cd71bcdde7 update bounties link 2025-02-13 07:00:45 -05:00
woodser
bd3fffada4 add hyperlink to dispute resolution in TAC window 2025-02-12 06:35:20 -05:00
woodser
bffcf7c7c0 fix hyperlinks to payment methods 2025-02-12 06:20:55 -05:00
woodser
c26974610c always copy monero binaries to resources folder on build 2025-02-09 10:15:01 -05:00
woodser
5f8cf97d16 replace throwing Error with RuntimeException 2025-02-09 08:26:23 -05:00
woodser
afa95f1b15 fix mismatch between payment sent message state and trade state 2025-02-07 07:38:44 -05:00
woodser
728cf22578 replace 'mediator or arbitrator' with 'arbitrator' for some translations 2025-02-05 10:44:18 -05:00
woodser
f35c7f8544 remove account info from crypto offer view 2025-02-05 10:44:09 -05:00
woodser
b48dbc2fb3 fix broken link to report issues 2025-02-05 08:06:32 -05:00
woodser
8038dcf401 remove HRK currency from paysafe 2025-02-04 09:54:40 -05:00
woodser
ae8760d72c add paysafe payment method 2025-02-02 08:11:01 -05:00
woodser
71fab722ee remove make offer to unsigned account warning 2025-01-31 09:02:54 -05:00
woodser
c333803917 fix npe duplicating offer with deleted payment account 2025-01-31 09:02:54 -05:00
woodser
e6b29b88f5 replace issues link from bisq to haveno 2025-01-31 07:56:55 -05:00
woodser
352384b41e log expected vs actual maker fee on error 2025-01-30 08:14:59 -05:00
woodser
12def5f1b5 remove extra input from tab navigation in create offer view 2025-01-29 09:42:14 -05:00
woodser
88c3f04be0 enable floating price offers for cardless cash 2025-01-29 09:24:42 -05:00
woodser
97569bad37 do not require extra info in cardless cash form 2025-01-29 09:24:42 -05:00
woodser
1d4dbe7ce0 increase rate limit to get offers to 3 per second 2025-01-27 09:32:40 -05:00
woodser
a014740014 rename TransferWise to Wise 2025-01-27 09:32:23 -05:00
woodser
9b50cd7ba7 fix contract mismatch by nullifying extra info empty string 2025-01-26 21:04:33 -05:00
woodser
6c6c6e2dd5 support additional info on all offers 2025-01-26 09:33:18 -05:00
woodser
a6af1550a4 set arbitrator payment account payloads on dispute opened 2025-01-25 08:24:45 -05:00
woodser
e4714aab89 display N/A for buyer contact in seller step 2 form 2025-01-25 08:24:45 -05:00
woodser
0d2c1fe8fd show extra info popup on take f2f offer 2025-01-25 08:24:45 -05:00
woodser
dc7a8e4201 do not override trade currency on country-based form deserialization 2025-01-25 08:24:45 -05:00
woodser
c3f7f194b0 AliPay supports all supported currencies 2025-01-22 11:01:02 -05:00
woodser
3847d1bd3a WeChat Pay supports CNY, USD, EUR, and GBP 2025-01-22 11:00:51 -05:00
woodser
535b71adc5 remove unused imports 2025-01-19 14:38:19 -05:00
woodser
66770cc98f use fixed localization for parsing offer amounts 2025-01-19 14:28:56 -05:00
woodser
39bc54df73 bump version to 1.0.18 2025-01-19 08:57:26 -05:00
woodser
7bc341d69f rename 'Cash at ATM' to 'Cardless Cash' 2025-01-18 10:19:04 -05:00
woodser
9a74856fa2 increase trade limit of no deposit offers to 1.5 xmr 2025-01-18 07:51:07 -05:00
woodser
a8fb638594 align extra info textarea in column for cash at atm buyer step 2025-01-18 07:47:20 -05:00
woodser
2d4455b1a2 update atomic unit conversion utils to use monero-java 2025-01-17 17:40:58 -05:00
woodser
bf8f4cea73 update to monero-java v0.8.35 2025-01-17 17:40:58 -05:00
woodser
fac901331f always round offer amounts to 4 decimal places 2025-01-17 14:52:17 -05:00
woodser
130a45c99a serialize payment account form lists to comma delimited string 2025-01-17 08:35:13 -05:00
woodser
b571b39790 support --xmrBlockchainPath startup flag for local Monero node 2025-01-16 08:32:24 -05:00
boldsuck
88b6bed93e
Upgrade GH workflows to remove deprecation notices (#1545) 2025-01-15 15:28:59 -05:00
woodser
97475d84e9 use ubuntu 22.04 for all github actions 2025-01-15 09:47:05 -05:00
woodser
69da858365 check for best connection before returning singular connection 2025-01-14 14:30:14 -05:00
woodser
e1b3cdce28 move version to last on password and startup screen 2025-01-14 11:51:38 -05:00
woodser
7fba0faac1 best connection defaults to singular instance 2025-01-14 11:08:48 -05:00
woodser
5e6bf9e22b fix fallback prompt with null daemon connection 2025-01-14 11:08:48 -05:00
woodser
0f5f7ae46e add startup flag 'updateXmrBinaries=true|false' 2025-01-14 11:07:35 -05:00
woodser
6301bde10e replace Thread.dumpStack() to write stack traces to log files 2025-01-14 07:34:36 -05:00
woodser
2f322674f8 fix showing extra info in offer details 2025-01-13 16:01:02 -05:00
boldsuck
533527e362
Update Tor browser version 14.0.3 and tor binary version 0.4.8.13 (#1534) 2025-01-11 13:27:53 -05:00
woodser
b0c1dceb56 render offer view after main thread loaded 2025-01-10 07:15:03 -05:00
woodser
d9f9c1e736 do not restore backup wallet cache if shutting down 2025-01-10 07:14:55 -05:00
woodser
1ac4c45f6d ignore task cancelled error in broadcast handler after shut down 2025-01-10 07:14:55 -05:00
woodser
e426f4d8f1 update to monero-java v0.8.34 2025-01-08 16:04:15 -05:00
woodser
944c189166 show CashApp payment method field in account form 2025-01-07 09:35:12 -05:00
woodser
e8d5366941 load offer book views off main thread #1518 2025-01-07 06:51:49 -05:00
woodser
3e0b694e13 update translations to register filter object 2025-01-07 06:38:52 -05:00
slrslr
21ea08a68d Update displayStrings_cs.properties 2025-01-07 06:35:07 -05:00
woodser
a1a7f9ccc9 Revert "update translations to register filter object"
This reverts commit 3860ce942c9e05318d0b809e166c345ad187f284.
2025-01-07 06:34:54 -05:00
woodser
e4f3d13660 penalize menu only appears for arbitrator in failed trades view 2025-01-07 06:26:45 -05:00
woodser
25f85f9f8d update translations to register filter object 2025-01-07 06:26:24 -05:00
woodser
a9325356c4 update translations for higher chargeback warnings 2025-01-06 09:12:50 -05:00
woodser
9e95de2d7e save and backup wallet files once per 5 minutes on polling 2024-12-29 09:36:32 -05:00
woodser
0462ddc273 backup wallet files before saving 2024-12-29 09:36:32 -05:00
woodser
c1b17cf612 update p2p table on user thread to fix null scene 2024-12-29 08:54:02 -05:00
woodser
89007c496e fix connected status in network settings for current connection 2024-12-29 08:54:02 -05:00
woodser
2dc7405f82 log connection read timeouts at info level 2024-12-29 08:53:03 -05:00
phytohydra
6a798312fe Add version number to splash screen, update version in pw dialog to have a leading "v" 2024-12-29 07:55:41 -05:00
woodser
fc1388d2f4 fix npe accessing funding address entry from api 2024-12-27 11:58:23 -05:00
woodser
cccd9cf094 fix null wallet on error handling 2024-12-27 10:32:45 -05:00
woodser
018ac61054 show reserved balance for offer funding subaddresses and reset if unused 2024-12-27 09:19:22 -05:00
phytohydra
ed87b36a76 Add Haveno version to password dialog 2024-12-27 09:18:39 -05:00
woodser
adcf158a90 show security deposit from trade amount in take offer and trade details 2024-12-24 09:59:16 -05:00
woodser
f053a274a4 bump version to 1.0.17 2024-12-21 09:19:18 -05:00
woodser
fdee044023 fix occasional miscolored buttons to remove or edit my offer 2024-12-21 09:17:06 -05:00
woodser
42ede83ca2 'show all' resets default currency to create new offer 2024-12-21 09:16:57 -05:00
woodser
5444d96832 reverse order of funds > confirmations and memo columns 2024-12-21 09:16:48 -05:00
woodser
7340ca9c21 allow scheduling funds from split output tx 2024-12-21 09:16:40 -05:00
woodser
542441d9d2 increase contrast of filter toggles and remove bottom highlight 2024-12-21 09:16:28 -05:00
woodser
c5ef60ce5c fix ui to set security deposit pct w/o deposit 2024-12-21 09:16:28 -05:00
woodser
389c5dddac fix no deposit filter applied to sell tab 2024-12-21 09:16:28 -05:00
woodser
7240b5f222 document changing download url for network deployment 2024-12-21 08:45:16 -05:00
woodser
34e0c4b71f remove bitcoin donation address from readme 2024-12-20 09:36:57 -05:00
woodser
aab4d0207e update links to typescript client and tests 2024-12-20 06:43:14 -05:00
woodser
1a51b171a0 bump version to 1.0.16 2024-12-19 16:21:44 -05:00
woodser
a557d90e5d fix password prompt on startup by referencing lock@2x.png 2024-12-19 16:16:22 -05:00
woodser
7e4e950710 update flatpak release date 2024-12-19 06:58:34 -05:00
woodser
323d14feb0 bump version to 1.0.15 2024-12-19 06:04:55 -05:00
woodser
5c79380e63 remove padding from no deposit slider 2024-12-18 12:15:11 -05:00
woodser
af3c7059a9 play chime when buyer can send payment 2024-12-18 11:06:39 -05:00
woodser
7e29dc188d fix scheduling offers by computing spendable amount from txs 2024-12-17 13:04:23 -05:00
woodser
544d69827a show locked symbol for private offers in trade history 2024-12-17 13:04:23 -05:00
woodser
c75e3aa455 replace checkbox to reserve necessary funds with slider 2024-12-17 07:17:29 -05:00
woodser
bd5accb5a5 update translations for reserving only necessary funds 2024-12-16 10:58:40 -05:00
woodser
775fbc41c2 support buying xmr without deposit or fee using passphrase 2024-12-16 10:20:56 -05:00
woodser
ece3b0fec0 fix concurrency exception updating capabilities #1473 2024-12-16 09:52:57 -05:00
woodser
4b7db9a1ae remove colon from disputed payout transaction id 2024-12-16 09:38:50 -05:00
woodser
140961d885 show either dispute payout tx id or normal payout tx id 2024-12-16 09:34:40 -05:00
woodser
9ec2794931 do not auto complete trades resolved by arbitration 2024-12-15 11:53:28 -05:00
woodser
85acb8aeb3 fix sorting of dispute state column 2024-12-15 10:52:17 -05:00
woodser
19398bb73e throttle warnings in KeepAlive and PeerExchange handlers #1468 2024-12-15 08:55:48 -05:00
woodser
0275de3ff6 increase limits: crypto to 528, very low risk to 132, pay by mail to 48
Co-authored-by: XMRZombie <monerozombie@proton.me>
2024-12-14 11:53:16 -05:00
slrslr
b586bc57f6
Fixing some words displayStrings_cs.properties (#1454) 2024-12-05 10:30:19 -05:00
woodser
7f6d28f1fb prompt to fall back on startup error with custom node 2024-12-02 13:55:56 -05:00
woodser
1aef8a6bab fix deadlock on startup while awaiting monero connection 2024-12-02 13:55:56 -05:00
woodser
71987400c7 make or take offer applies wallet funds and computes remaining amount 2024-12-02 13:55:38 -05:00
woodser
e05ab6f7ed fix links from offer book chart to buy/sell views 2024-12-02 13:55:18 -05:00
woodser
cfaf163bbc update account limit hyperlinks 2024-11-30 18:49:51 -05:00
woodser
dc8d854709 show available monero nodes in network settings 2024-11-29 12:31:09 -05:00
woodser
1f385328de increase rate limit to get offers on testnet 2024-11-25 11:50:53 -05:00
woodser
98e2df3c7e fix scheduling offers with funds sent to self 2024-11-25 11:05:42 -05:00
woodser
103c45d412 fix showing offer created popup after canceled 2024-11-25 11:05:42 -05:00
woodser
c9cf5351c0
support usdc (#1439) 2024-11-25 10:48:27 -05:00
coinstudent2048
bf452c91da
add flatpak version (#1429) 2024-11-25 10:42:09 -05:00
ohchase
a5417994d6
flatpak icon support (#1428) 2024-11-25 10:40:27 -05:00
woodser
c40e0bea5a build instructions warn that mainnet is not supported 2024-11-24 11:31:02 -05:00
ohchase
ae80935f3a enable hidden files in cache node dependencies 2024-11-24 08:59:55 -05:00
woodser
68b4a0fafb update links to #haveno-development 2024-11-23 14:20:33 -05:00
woodser
8fd7f17317 update translation for funding wallet on take offer 2024-11-20 10:09:42 -05:00
woodser
24657c6c57 update translation for funding wallet on create offer 2024-11-20 10:09:42 -05:00
woodser
ba763f7bf6 update hyperlink to f2f payment method 2024-11-17 10:36:56 -05:00
woodser
264cb5f0ac fix inverted buy/sell label on make or take crypto offer 2024-11-15 10:42:08 -05:00
woodser
c9e992442c bump version to 1.0.14 2024-11-15 09:27:36 -05:00
woodser
86e67d384c new dispute state is considered open 2024-11-15 09:21:41 -05:00
woodser
59d8a8ee44 trader can re-open dispute unless payout confirmed 2024-11-15 09:21:41 -05:00
woodser
5221782ba0 return empty list if no backup files exist 2024-11-14 23:43:10 -05:00
bvcxza
023e2bcd2f
Fix deviation percent for fixed-price crypto offers (#1411) 2024-11-14 10:16:13 -05:00
woodser
ea8badd3f6 bump version to 1.0.13 2024-11-11 20:46:44 -05:00
woodser
7a392f3c1e defer payout publish if published, skip warning log if published 2024-11-11 13:39:24 -05:00
woodser
dd5b7996b3 fix price provider selection, remove local provider for production 2024-11-11 07:29:52 -05:00
woodser
d906ffa36a remove USDT-TRC20 from 'main' cryptos 2024-11-11 07:29:52 -05:00
woodser
3cdfa0fa27 fix equals and hashcode of trade, crypto, and traditional currencies 2024-11-11 07:29:52 -05:00
woodser
9c3e405fe0 get market price by currency code base e.g. usdt 2024-11-10 17:33:49 -05:00
woodser
2c3cd5208b support USDT-ERC20 and USDT-TRC20 2024-11-10 12:53:59 -05:00
woodser
0450900b37 validate pending offer 2024-11-10 11:19:57 -05:00
woodser
8f5e56b9dc prefer local price provider, preserve provider when banned nodes applied 2024-11-10 11:17:44 -05:00
woodser
0d82f8827f dispute is pending until requested and chat disabled until acked 2024-11-10 11:12:44 -05:00
woodser
577cfa249e try opening wallets with all backup cache files then no cache file 2024-11-10 11:12:44 -05:00
woodser
b348a81f13 increase number of wallet backups to 2 2024-11-10 11:12:44 -05:00
woodser
8a9b4ffe11 backup trade wallet on successful save and close 2024-11-10 11:12:44 -05:00
woodser
466e1f048e fix bug restoring wrong wallet cache 2024-11-10 11:12:44 -05:00
woodser
1af87b2db9 do not initialize failed trades until moved back to pending trades 2024-11-10 11:12:44 -05:00
woodser
cdb99a9cfb support opening dispute with corrupt trade wallet 2024-11-10 11:12:44 -05:00
woodser
6db4812f06 synchronize on lock to prepare payment sent or received 2024-11-10 11:12:10 -05:00
woodser
5c4fa7a53f import multisig hex if needed on create payout tx 2024-11-10 11:12:10 -05:00
woodser
74e094fa99 poll trade wallet on error importing multisig or processing payout 2024-11-10 11:11:28 -05:00
woodser
329fa1c39a set poll in progress from single caller at a time 2024-11-10 11:11:28 -05:00
woodser
7094dfcc09 always poll trade wallet on sync and poll 2024-11-10 11:11:28 -05:00
woodser
5d85335dc7 show seller's receive address for crypto in trade step view 2024-11-10 09:12:10 -05:00
woodser
6730d023d6
describe how payment account details are stored and shared #1359 (#1387)
Co-authored-by: rafau <adas.rafalowski@gmail.com>
2024-11-10 08:44:59 -05:00
XMRZombie
b31758e884
improve robustness and clarity of SingleThreadExecutorUtils.java (#1378) 2024-11-10 08:39:12 -05:00
woodser
2a45ebe565 refactor buy/sell tab labels #1351 2024-11-10 08:37:33 -05:00
woodser
0c76c48c65 refactor buy/sell tab functionality #1351 2024-11-10 08:37:33 -05:00
woodser
f470112cae fix FX application thread error in portfolio view #1356 2024-11-10 08:36:32 -05:00
woodser
123a2a8487 log error on infinity in offer book chart view without popup #1340 2024-11-10 08:36:10 -05:00
woodser
bc1cfe3ba0 exclude unsigned offers from republish to fix warning 2024-11-10 08:35:08 -05:00
woodser
ed567beeb3 add logging for dispute's trade being null #1364 2024-11-10 08:32:48 -05:00
woodser
22f32f43a0 fix mouse pointer on tab hover in light mode 2024-11-10 08:32:35 -05:00
woodser
e3420de0d8 increase contrast of main tab labels in light mode 2024-11-10 08:32:17 -05:00
woodser
a9c975466e log warning on failure to sign and publish peer account age witness 2024-11-08 08:36:43 -05:00
woodser
072401386e improve sound compatibility for linux 2024-11-08 08:02:55 -05:00
woodser
a00930aa9e focus on password input in password dialog window 2024-11-08 08:02:42 -05:00
woodser
b0fc864313
remove '-x socks5h' and update url for tails install (#1385)
Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2024-11-07 05:56:24 -05:00
nahuhh
ee889c5238
scripts: use wget for tails setup (#1381) 2024-11-07 05:54:52 -05:00
woodser
aee0c1c0b2 validate form fields on create payment account from form 2024-11-05 10:37:58 -05:00
niyid
485746381c
Memo sorting fix (#1354) 2024-11-01 06:14:30 -04:00
niyid
c855d66a0c
fix sorting on buy > amount column ( 2024-10-31 15:06:14 -04:00
bvcxza
6b91b057e5
fix background color of txid when funds are withdrawn in dark mode (#1367) 2024-10-31 10:01:00 -04:00
woodser
6675390e20 update price node address 2024-10-30 15:31:40 -04:00
XMRZombie
4821a8d284
test taproot address for Bitcoin (#1362) 2024-10-30 09:26:08 -04:00
woodser
e0cdef8844 update installing section in readme 2024-10-26 14:12:21 -04:00
woodser
3520694251 update tails instructions from gr8ful4 2024-10-26 01:36:31 -04:00
woodser
e0244a51dc update readme: add run section, remove quality grade, fix bounty links 2024-10-26 01:22:16 -04:00
woodser
6b3e9febf3
bump version to 1.0.12 (#1329) 2024-10-13 12:06:34 -04:00
woodser
6ff73f1898
standardize installer artifact naming conventions (#1328) 2024-10-13 12:06:11 -04:00
woodser
e811e2b224
improve error handling on payment received message (#1327) 2024-10-13 08:32:26 -04:00
woodser
a6c178c058
deduplicate additional trade statistics (#1326) 2024-10-13 08:31:59 -04:00
woodser
cda610fdfd
close mutable offer view on fx thread (#1325) 2024-10-13 08:31:49 -04:00
woodser
6b688194f1
play chime when buyer can confirm payment sent (#1324) 2024-10-13 08:31:41 -04:00
woodser
5f0d95c743
log height of local monero node (#1322) 2024-10-13 08:31:32 -04:00
woodser
5352225bce
log error on failure to set password (#1318) 2024-10-12 06:18:53 -04:00
woodser
58787a1d31
reclassify public nodes and use if no provided nodes available (#1315) 2024-10-12 06:18:16 -04:00
woodser
ddb48d1846
enable connection auto switch on daemon by default (#1320) 2024-10-12 06:18:06 -04:00
woodser
264741152a
enable sounds by default for existing clients 2024-10-10 09:13:00 -04:00
woodser
a4223a6aab updates to flatpak installer 2024-10-08 14:02:40 -04:00
Jabster28
662eaee7c3 feat: add flatpak builds (#1230) 2024-10-08 14:02:40 -04:00
woodser
a53e6a0e3d
update to monero-project v0.18.3.4 2024-10-07 11:28:32 -04:00
woodser
ebc28805c8
support getting connection auto switch flag from grpc api
get connection auto switch flag from grpc api
2024-10-07 09:21:23 -04:00
woodser
a1b50a7b42
show transaction details popup with tx key #1309 (#1311) 2024-10-06 12:01:55 -04:00
woodser
e25d3fb52e
update tails installation script as one-liner (#1312) 2024-10-06 08:55:12 -04:00
bvcxza
dbc841979b
fix offer buttons hard to read (#1310) 2024-10-05 08:03:04 -04:00
woodser
d61eabc3d2 schedule offer using available and pending funds 2024-10-03 12:22:04 -04:00
woodser
5c0f49d58f do not reset offer address entries on error from desktop ui form 2024-10-03 12:22:04 -04:00
woodser
047f1a1c1f
add os and installer to installer artifacts 2024-10-02 11:03:45 -04:00
woodser
7fe27b26b2
update packaging docs for AppImage (#1306) 2024-10-02 10:39:32 -04:00
TheTollingBell
61158e9750
support AppImage packaging (#1270) 2024-10-02 10:36:29 -04:00
woodser
ea3f099df7 increase sync progress timeout to 120s 2024-10-01 06:40:01 -04:00
woodser
8696c71051 update naming convention of artifact archives and installers
Co-authored-by: justynboyer@gmail.com <justynboyer@gmail.com>
2024-09-30 11:44:43 -04:00
woodser
b940021d99 play sounds on notifications #1284 2024-09-30 10:39:15 -04:00
woodser
b2a6708ac1 sync blockchain depending on last used local node 2024-09-30 10:14:41 -04:00
woodser
3ffda0fdd1 fix account export and import without key ring #1221 2024-09-30 10:11:47 -04:00
woodser
3e3f3085f8 fix not enough signers on process payout tx 2024-09-30 10:11:39 -04:00
woodser
60b91d3d23 fix build artifacts for ubuntu-22.04 2024-09-30 09:40:24 -04:00
justynboyer@gmail.com
11c0f7613b fix(ci): pin ubuntu ci version 2024-09-26 10:19:00 -04:00
woodser
1329902a55 add transaction fee column to funds > transactions
Co-authored-by: niyid <neeyeed@gmail.com>
2024-09-26 10:10:31 -04:00
woodser
6c640ddbef resize donation qrs 2024-09-26 08:29:02 -04:00
woodser
50f3bd510a log stack traces at warn or error level 2024-09-25 09:40:50 -04:00
woodser
8d55abe3b9 add deprecated tails support as backup 2024-09-23 18:56:27 -04:00
woodser
c04fc7b2db save multisig wallet on same thread in trade wallet operations 2024-09-17 12:23:34 -04:00
woodser
0ed640be16 save multisig wallet on same thread when initialized 2024-09-17 12:23:34 -04:00
woodser
2a9bc87f65 check multisig state on initialization 2024-09-17 12:23:34 -04:00
woodser
2f310b420d
use error level for error log file 2024-09-17 12:00:35 -04:00
preland
d4f1dc5b8e
Create error log file 2024-09-17 11:44:20 -04:00
woodser
a4e43f1045 update to monero-java v0.8.33 2024-09-16 21:12:53 -04:00
woodser
7306972d19 fix reference to backup cache file 2024-09-15 09:41:10 -04:00
woodser
1ea9a6f750 add notes for tails installation 2024-09-12 12:11:11 -04:00
woodser
c08b4d0772
print stack trace on error closing dispute ticket 2024-09-12 11:41:26 -04:00
woodser
874075b371
add curl command for tails install script 2024-09-09 09:13:47 -04:00
woodser
6d1f1e43d6
update installation process for tails
Co-authored-by: anonymous
2024-09-09 07:34:39 -04:00
zabaniz
e4f0277326 Makefile, add deploy-tmux and deploy-screen 2024-09-07 09:25:14 -04:00
woodser
dbbfb50cd3 update bounty description 2024-09-06 10:22:48 -04:00
woodser
a20377fb04 bump version to 1.0.11 2024-09-04 08:40:57 -04:00
woodser
a8e76fd720 automatically restore backup cache if corrupt 2024-09-04 05:37:03 -04:00
woodser
2d0f200aa1 do not reprocess error on submit multisig tx w/o enough signers #1227 2024-09-04 05:36:51 -04:00
woodser
cae360b6c5 throttle warnings when monerod not synced within tolerance 2024-09-04 05:36:31 -04:00
woodser
30f0cf273c resize donation qr codes 2024-09-01 12:33:41 -04:00
woodser
af44544e15 add donation qr codes with logos 2024-09-01 12:25:08 -04:00
woodser
6759687046 fix logs for repeated attempts 2024-08-28 14:53:32 -04:00
woodser
d74b0995d0 update build instructions for mainnet 2024-08-24 14:10:58 -04:00
woodser
caebd58303
preserve withdraw fields when page reactivated while in progress 2024-08-20 08:30:48 -04:00
Rico
e18d281ca9
Add "UI Scaling" section to the user guide 2024-08-20 08:30:03 -04:00
woodser
d3ee7340f0 remove jtorctl from JesusMcCloud 2024-08-16 17:24:24 -04:00
woodser
d3d30c3d0b exclude jtorctl from JesusMcCloud globally 2024-08-16 16:32:18 -04:00
woodser
8cb8f9f3cf exclude jtorctl from JesusMcCloud 2024-08-16 14:18:51 -04:00
woodser
1046caf907 update jtorctl version 2024-08-16 13:54:37 -04:00
woodser
5f6f513f3c Revert "remove jtorctl from verification metadata"
This reverts commit b3d3a936d567bb59a12695593e9a2772aae15700.
2024-08-16 12:19:53 -04:00
shortwavesurfer2009
ffe88b49a6
Lower PoW activation threshold 2024-08-14 07:47:20 -04:00
woodser
38b1ace4a4
document changing p2p network code for new networks 2024-08-11 19:27:31 -04:00
woodser
b3d3a936d5
remove jtorctl from verification metadata 2024-08-11 19:27:19 -04:00
bvcxza
a7e90395d2
Fix build failure removing jtorct explict dependency 2024-08-10 13:53:15 -04:00
woodser
dca53533f0
re-add torcc config for seednode2 2024-08-09 20:20:28 -04:00
woodser
74fdf32c2a
clean up tor config and documentation 2024-08-09 10:37:17 -04:00
Ikko Eltociear Ashimine
0461fe66ec
fix typo in XmrWalletService.java, excute -> execute 2024-08-09 10:37:01 -04:00
woodser
a7cda6cdeb update readme 2024-08-08 10:14:58 -04:00
woodser
0644d0b74a fixes to sync with progress start and target height 2024-08-07 08:47:27 -04:00
woodser
e10f764dc2 bump version to 1.0.10 2024-08-07 07:13:37 -04:00
woodser
7461da9722 request connection switch on poll if txs not updated within 3 minutes 2024-08-07 07:13:37 -04:00
woodser
d300ce8ce7 announce connection change on refresh period change 2024-08-07 06:57:43 -04:00
woodser
fa25843684 save wallet on each poll 2024-08-07 06:57:43 -04:00
woodser
0105c1436a sync wallet directly within 100 blocks of target height 2024-08-07 06:57:43 -04:00
woodser
443c2f4cdb sync on wallet lock before checking if wallet is behind 2024-08-07 06:57:43 -04:00
woodser
3dfaa2fc52 fix concurrent modification exception in disputes list 2024-08-07 06:57:43 -04:00
woodser
d9a3feba8d use lock for importing multisig hex 2024-08-07 06:57:43 -04:00
woodser
ca7d596175 refactor base wallet so trades can sync with progress timeout, etc 2024-08-07 06:57:43 -04:00
woodser
1b5c03bce8 refactor syncing wallet with progress and switching connections 2024-08-07 06:57:43 -04:00
woodser
dbb3d4f891 offer key images are null when not initialized 2024-08-07 06:57:43 -04:00
woodser
d9630a13b5 cancel tx on dispute summary window re-enables buttons 2024-08-03 11:38:04 -04:00
woodser
00ceeeba5f do not allow arbitrators to open disputes 2024-08-03 11:37:54 -04:00
woodser
9004c7f32a fix re-signing offers with inexact amount reserved 2024-08-03 11:37:45 -04:00
woodser
2e57e6197d release lock processing offers before callback 2024-08-03 11:37:45 -04:00
woodser
76f8f85487 republish offers unless deactivated or canceled 2024-08-03 11:37:45 -04:00
woodser
8d8aa65d1d remove warning checking if can take offer by arbitrator signature 2024-08-03 11:37:45 -04:00
woodser
0df4f63d53 show top error on failure to open main wallet 2024-08-03 11:37:29 -04:00
woodser
3b0080dbba support re-opening dispute if payout fails 2024-08-03 11:37:20 -04:00
woodser
79cd9f3e82 re-export multisig hex on create multisig tx or open dispute 2024-08-03 11:37:20 -04:00
boldsuck
d4a9838cd8
update documentation and config for external tor w/ pow defense
Co-authored-by: shortwavesurfer2009 <116814522+shortwavesurfer2009@users.noreply.github.com>
Co-authored-by: fa2a5qj3 <174058787+fa2a5qj3@users.noreply.github.com>
Co-authored-by: preland <prelandofficialmusic@gmail.com>
2024-08-02 08:27:10 -04:00
bvcxza
0f0b645f72
use AutocompleteComboBox to create new payment method 2024-08-01 07:46:38 -04:00
woodser
cf282fd930
clean up for internal/external tor changes 2024-07-29 07:57:47 -04:00
fa2a5qj3
3d44f3777c
Support tor w/either Netlayer or direct bind to SOCKS5 data port 2024-07-29 07:54:53 -04:00
woodser
84a2828e90
update documentation for tails 2024-07-28 09:53:31 -04:00
woodser
c63cf2f0a0
remove duplicate historical trade stats after fuzzing 2024-07-28 09:20:50 -04:00
woodser
75b96e83da
throttle log warnings in Connection.java to 30s 2024-07-28 09:20:35 -04:00
bvcxza
180fde87cc
#1130 change graph lines to lighter gray in dark mode 2024-07-26 11:36:05 -04:00
phytohydra
a8d5c63f9f
Remove excess transparency from around icons 2024-07-24 14:26:51 -04:00
woodser
5ca9cb8dff
increase wallet sync progress timeout to 60s 2024-07-24 09:42:00 -04:00
bvcxza
ddee87f85d
Fix popups text background color 2024-07-23 08:26:02 -04:00
woodser
1e70c70579
fix publishing duplicate trades after randomization 2024-07-23 08:05:41 -04:00
woodser
bdcf8a2182
fix npe force closing wallet on startup 2024-07-23 08:03:36 -04:00
shortwavesurfer2009
d8161cd6e2
fix missing http in seed node service file 2024-07-21 23:08:09 -04:00
woodser
629e1508f2
do not request connection change on trade poll error except rescan 2024-07-20 21:41:20 -04:00
woodser
b03c873a06
search by payment method in filter and tweak ui 2024-07-20 21:39:10 -04:00
PW
7c8753c17b
Make offers filterable via offer ID or Node address 2024-07-20 21:29:57 -04:00
woodser
b61f1fabcd disable synchronizing on daemon for performance 2024-07-20 18:20:10 -04:00
woodser
13d6eaee7d recover from offer funds being unexpectedly unavailable 2024-07-20 18:20:10 -04:00
woodser
fc3407cd50 ignore switch connection request within 10s, 2 per min 2024-07-20 18:20:10 -04:00
woodser
eb8025f6e8 skip wallet checks on deletion if deposit not requested 2024-07-20 18:20:10 -04:00
woodser
5d739f912c remove dedicated connection change thread for trade 2024-07-20 18:20:10 -04:00
woodser
82d586ab78 remove trade on error force closes and shuts down thread 2024-07-20 18:20:10 -04:00
woodser
291622e452 switch to current connection on connection changed 2024-07-20 18:20:10 -04:00
woodser
9b26682646 fixes checking for missing wallet data 2024-07-20 18:20:10 -04:00
woodser
09fd8710b1 do not set offer state to pending after canceled 2024-07-20 18:20:10 -04:00
woodser
ebcadb7bed reinitialize main wallet on same thread as connection change 2024-07-20 18:20:10 -04:00
woodser
cb132e727a limit switch connection requests to 3 per minute 2024-07-20 18:20:10 -04:00
woodser
41e63805c1
cleanup instructions to install dependencies in deployment guide 2024-07-19 19:15:35 -04:00
shortwavesurfer2009
16263bb7b3
Update deployment documentation (#1152) 2024-07-19 19:07:31 -04:00
woodser
326cfdfb80 Revert "enable proof of work dos protection in torrc"
This reverts commit c22c3b82dd43b66b30e67ac25b2488b704fb83b3.
2024-07-19 09:07:23 -04:00
woodser
0b3763f900 fix 'not enough money' bug by trying any subaddress 2024-07-19 08:22:48 -04:00
woodser
7308206a10 set trade role from utility class in api #1146 2024-07-18 07:18:25 -04:00
woodser
caaf9f7b5b limit logging of poll errors to once every 4 minutes 2024-07-18 07:18:15 -04:00
woodser
8b7e95f614 reduce logging on failure to sign offer 2024-07-17 15:35:33 -04:00
woodser
d69dcae875 preserve offers unless invalid #1115 2024-07-17 15:35:33 -04:00
woodser
06b0c20bad switch to next best monerod on various errors 2024-07-17 15:35:22 -04:00
woodser
33bf54bcac recover from payment received message not processed 2024-07-17 15:35:22 -04:00
woodser
1c381de806 add mainnet monero nodes 2024-07-17 15:35:22 -04:00
woodser
78a2476bb8 decrease startup timeout to 4 mins 2024-07-17 15:35:22 -04:00
woodser
96bcd7547d ignore error syncing with progress on shut down 2024-07-17 15:35:22 -04:00
woodser
ff0ccc21e2 fix error popup on delete outdated tor files 2024-07-17 15:35:22 -04:00
woodser
9c359b5e29
support deleting payment accounts #1136 2024-07-16 16:11:50 -04:00
fa2a5qj3
0a469db8f6
randomize completed trade info, fixes #1099 2024-07-16 16:08:55 -04:00
Milan Hauth
40421eec75 switch platform for script files 2024-07-16 16:07:56 -04:00
woodser
690f38e4dd bump version to 1.0.9 2024-07-16 12:30:52 -04:00
woodser
91e442d642 simplify the filter object message 2024-07-16 07:52:02 -04:00
woodser
23a7fb3d16 cache wallet info last on poll 2024-07-13 11:49:33 -04:00
woodser
a149d92392 sleep after restarting seednode service 2024-07-13 11:49:21 -04:00
woodser
4a6fcfae84 limit logging of invalid connection requests 2024-07-13 11:49:21 -04:00
woodser
c1c8c6fa85 remove extra quote from java opts 2024-07-13 11:49:21 -04:00
woodser
c2b816e5f0 log when offer's signing arbitrator is unavailable 2024-07-13 11:49:21 -04:00
woodser
f852217945 adjust presentation of extra info in buyer step screen 2024-07-13 09:09:10 -04:00
woodser
255bd33c47 increase chat message limit to 4 per minute 2024-07-13 09:08:59 -04:00
woodser
082c8c4290 Revert "randomize completed trade info, fixes #1099"
This reverts commit db6cb237bff8435cb156a344b7a554b5bde772a5.
2024-07-13 07:38:36 -04:00
woodser
9747b20a27 show error message on error confirming payment received 2024-07-12 14:42:14 -04:00
Shortwavesurfer2009
b0c73d1b39 update to ipv6 address in seednode service 2024-07-11 11:54:55 -04:00
fa2a5qj3
db6cb237bf randomize completed trade info, fixes #1099 2024-07-10 10:29:04 -04:00
woodser
8e3e91c7cc remove bitcoin from makefile #1120 2024-07-10 08:39:40 -04:00
woodser
6de61243b3 set selected currency when account has single currency 2024-07-08 11:54:11 -04:00
woodser
7c2af064a3 fix case for paypal extra info 2024-07-08 11:54:00 -04:00
walkerp07
05b00727a5 Add an "extra_info" text area for CashApp 2024-07-08 11:36:30 -04:00
walkerp07
8bf8144709 Add an "extra_info" text area for PayPal 2024-07-08 11:36:30 -04:00
woodser
31ce183c83 fix invalid res lookup for UpholdForm 2024-07-08 10:35:08 -04:00
woodser
5d39eecd4f fix 'not enough money' error on reserve exact offer amount #1089 2024-07-08 08:47:12 -04:00
woodser
86e4f7b3f2 retry creating withdraw tx 5 attempts 2024-07-08 08:46:54 -04:00
woodser
ec9f91e014 fix error on shut down broadcasting to peers 2024-07-01 12:21:58 -04:00
woodser
8ce91aa0c1 fix error on shut down updating p2p connections table 2024-07-01 12:21:58 -04:00
woodser
c01d63490f
fix null price feed listener 2024-07-01 08:41:17 -04:00
fa2a5qj3
856faafd1c
fix IPv6 connectivity to XMR nodes #1076 2024-06-30 08:56:39 -04:00
woodser
1debdde33e
skip wallet polls if daemon not synced (#1080) 2024-06-30 07:36:25 -04:00
sraver
c3b93b6e75
replace miner fee toggles with 'send max' link for withdraw #1033
Co-authored-by: sraver <pyrdnx@pm.me>
Co-authored-by: woodser <woodser@protonmail.com>
2024-06-30 07:36:06 -04:00
PW
7acba27b9d
Do not select payment account currencies by default 2024-06-25 07:39:43 -04:00
PW
7ebc1bfc11
Allow word wrapping on "Accepted taker countries" field #989 2024-06-24 07:24:52 -04:00
nsec1
3cac6d7c69
#967 Text in Pop Ups selectable 2024-06-23 18:31:15 -04:00
woodser
0a67b9a423 bump version to 1.0.8 2024-06-23 13:52:00 -04:00
woodser
1ee5a628a8 update to monero-java v0.8.31 for updated linux libs 2024-06-23 13:47:42 -04:00
woodser
7a9e814145 update monero binaries release to fix aarch64 2024-06-23 13:47:42 -04:00
woodser
c29cdb8b0f import multisig hex individually if one is invalid 2024-06-23 13:47:42 -04:00
woodser
8cdd65e7dd install monero bins to local app directories and exclude from backup 2024-06-23 13:47:42 -04:00
woodser
748f698314 add logging on failure to install monero binaries 2024-06-23 13:47:42 -04:00
woodser
8e8b0cc7c0 update instructions to build monero bins for linux aarch64 2024-06-23 13:47:42 -04:00
muklavon
ee540d075b Update displayStrings_tr.properties
Corrected minor translation error.
2024-06-23 13:46:44 -04:00
muklavon
ac332dfd76 Update displayStrings_tr.properties
Minor translation errors corrected
2024-06-23 13:46:44 -04:00
muklavon
40e8c8b82c Update LanguageUtil.java
Just a minor debug
2024-06-23 13:46:44 -04:00
muklavon
fbb9c0f490 Update displayStrings_tr.properties
Adjusted some sentences.
2024-06-23 13:46:44 -04:00
muklavon
17c559d000 Update LanguageUtil.java
Turkish language moved to uncommented section.
2024-06-23 13:46:44 -04:00
muklavon
4fcd36edc4 Turkish Translation Added 2024-06-23 13:46:44 -04:00
woodser
cb0b6665f7 add and remove offers on user thread to fix illegal state #1031 2024-06-23 13:46:34 -04:00
woodser
26a5ffcb31 fallback from custom node to provided nodes on startup error #923
Co-authored-by: nsec1 <167650977+nsec1@users.noreply.github.com>
2024-06-23 13:46:22 -04:00
woodser
4494af8bc0 disable tor for private ip addresses to fix #1026 2024-06-23 13:46:09 -04:00
Twiddle
4819e5ebfa
Added scripts to run haveno on tails (#1036) 2024-06-23 13:44:42 -04:00
woodser
69a1e67da2 remove incomplete trades from trade summary to fix #1029 2024-06-23 13:43:23 -04:00
woodser
5108c22a29 seller creates unsigned payout tx on stale data error 2024-06-23 13:42:24 -04:00
nsec1
f454bbbf63
fix market price margin setted to zero in offer edition (#1062) 2024-06-21 13:02:49 -04:00
woodser
1f0e326a16 log warning on error installing monero bins to fix grpc client 2024-06-19 13:26:11 -04:00
woodser
438e8d41cb fixes to editing offers #1011 2024-06-10 07:51:02 -04:00
woodser
57948b36fd fix error about window already being closed due to faded popover 2024-06-10 07:51:02 -04:00
woodser
0c1ac28e26 update offer qr code on key strokes 2024-06-10 07:51:02 -04:00
preland
c22c3b82dd enable proof of work dos protection in torrc 2024-06-10 07:38:16 -04:00
woodser
9b3d78bd4b fix ui forms for paypal, cash app, venmo 2024-06-09 08:32:20 -04:00
woodser
7abbe1c36f remove regional cake wallet nodes 2024-06-09 07:15:06 -04:00
woodser
fea804086b support paypal, cashapp, venmo
Co-authored-by: preland <89992615+preland@users.noreply.github.com>
2024-06-09 07:14:56 -04:00
woodser
26c32a8ff4 bump version to 1.0.7 2024-06-07 07:38:23 -04:00
woodser
ca125dbc48 seller publishes trade statistics without checking peer capabilities 2024-06-07 07:38:15 -04:00
woodser
37af7e5338 show multisig deposit destination address in transactions view #982 2024-06-07 07:38:07 -04:00
woodser
99f41e0feb update to monero-java 0.8.29 to fix #995 2024-06-07 07:37:59 -04:00
woodser
31a587861a document network info lost if seed nodes go offline at same time 2024-06-07 07:29:44 -04:00
woodser
88290c9dff fix invalid signature when sign offer re-requested after timeout 2024-06-07 07:29:34 -04:00
woodser
e12ec197bf support backing up data directory on windows with shut down #996 2024-06-07 07:29:15 -04:00
woodser
f6c35ba6f3 support extra info for australia payid account #976 2024-06-07 07:29:01 -04:00
woodser
f252265ede seller signs payout tx unless illegal state, avoid recreating payout tx 2024-06-07 07:28:49 -04:00
woodser
dd28c237c9 get or create maker payout address entry on trade init #978 2024-06-04 08:19:34 -04:00
woodser
99bb29e0b0 fix navigation links from market view to offer view #974
Co-authored-by: nsec1 <167650977+nsec1@users.noreply.github.com>
2024-06-04 08:18:14 -04:00
woodser
b963fd501f fix contract mismatch taking sepa offer with sepa instant account #981 2024-06-04 08:15:10 -04:00
woodser
0919d92ce4 increase max question length for InteracETransfer to 160 characters #969 2024-06-03 10:19:19 -04:00
woodser
cf1fabd0bb fix jfx error loading SupportView 2024-06-03 10:19:19 -04:00
woodser
afcf5dd365 make filter object message more clear 2024-06-03 10:19:19 -04:00
nsec1
879ed9b10a #921 Make terms of acceptance full screen for readability and copy to clipboard icon 2024-05-28 10:48:52 -04:00
woodser
7c3e87ed38 update release date 2024-05-25 14:56:23 -04:00
woodser
dade4a3759 do not extend timeout on sign contract request 2024-05-25 13:38:41 -04:00
woodser
6dfa1841f8 force restart trade wallet on confirm payment if missing txs to fix #960 2024-05-25 13:38:41 -04:00
woodser
2f0ea48a31 fix jfx error in MainView 2024-05-25 13:38:41 -04:00
woodser
7e898ba23d set combined sync progress on user thread 2024-05-25 13:38:41 -04:00
woodser
14d17023a8 seller publishes trade statistics if trade amount transferred 2024-05-24 15:56:28 -04:00
woodser
3e2206cf5e add logging and popup when trade wallet returns null txs 2024-05-24 15:56:28 -04:00
woodser
b63227e8db reset address entries off thread on close take offer 2024-05-24 06:48:49 -04:00
woodser
64081b684c arbitrator nacks maker init trade request if trade exists 2024-05-24 06:48:49 -04:00
woodser
3b1961fb05 do not extend arbitrator timeout awaiting deposit txs 2024-05-24 06:48:49 -04:00
woodser
796603f82b thaw reserved outputs within task if canceled 2024-05-24 06:48:49 -04:00
nsec1
f2f5a6fd24
#919 Expand "Extra info" field on offers (#950) 2024-05-23 21:15:51 -04:00
woodser
4172cc72df avoid unused import by using List.of() 2024-05-23 17:10:00 -04:00
woodser
52be627cca adjust max limit for payment methods to default high risk 2024-05-23 15:01:37 -04:00
woodser
68a4c21b17 enable cancel button while placing an offer 2024-05-23 15:01:37 -04:00
woodser
35f275805b reprocess scheduled offers on new block 2024-05-23 15:01:37 -04:00
woodser
a1e554473a extend sign offer timeouts 2024-05-23 15:01:37 -04:00
woodser
48f05cca8c update to monero-java 0.8.27, fixes #912 2024-05-23 15:01:37 -04:00
woodser
36f7037dde clear connection service error message on success 2024-05-23 15:01:37 -04:00
preland
d55153bd36 update netlayer 2024-05-22 16:47:52 -04:00
woodser
0c5ed84996 fix npe with offer details volume before prices loaded 2024-05-22 08:34:30 -04:00
woodser
b66e6b1c12 remove download url from mandatory update message 2024-05-21 18:00:23 -04:00
woodser
66e9ac7d3c bump version to 1.0.6 2024-05-21 18:00:23 -04:00
woodser
9cd28a6bde fix connection to custom node on startup #945 2024-05-21 18:00:23 -04:00
woodser
1150d929af maker selects arbitrator (breaking change) 2024-05-21 18:00:14 -04:00
woodser
6df5296dcd deduplicate trade history before may 31, 2024 2024-05-20 15:04:48 -04:00
woodser
3fbb2f95d0 update chat view messages on user thread 2024-05-20 15:04:48 -04:00
woodser
2e605a590e
bump version to 1.0.5 (#937) 2024-05-19 10:02:37 -04:00
woodser
80db207a98
fix issues scheduling offer with exact input amount (#934) 2024-05-19 09:59:34 -04:00
preland
7885d95a4c
update tor and netlayer (#933) 2024-05-19 09:59:19 -04:00
woodser
7df40580a3 remove incorrect withdraw screen translations #74 2024-05-19 09:19:22 -04:00
woodser
b4369fbb9f fix default monero port in custom node input #928 2024-05-18 11:45:13 -04:00
woodser
bee93bf45f reset internal state and popup warning if main wallet swapped 2024-05-18 10:11:34 -04:00
woodser
ffdbe73693 delete monero binaries from backup to reinstall with permissions 2024-05-18 10:11:34 -04:00
woodser
82b8f58579 show popup with message on error starting monero wallet 2024-05-18 10:11:34 -04:00
woodser
ea0ce9b449 deduplicate early trades due to bug 2024-05-17 13:08:20 -04:00
woodser
36e2f8675c only republish seller trades 2024-05-17 12:40:02 -04:00
woodser
cfd89b6da1 update readme 2024-05-17 06:52:28 -04:00
woodser
76859f822e ignore failure after task runner canceled 2024-05-17 06:52:28 -04:00
woodser
58cead6035 do not log error polling wallet within 3m of success 2024-05-17 06:52:28 -04:00
woodser
e30a247898 update documentation to change default application folder name 2024-05-16 08:03:15 -04:00
woodser
64acf86fbe fix total amounts in spread view 2024-05-15 18:18:41 -04:00
woodser
113a94b1f9 fix concurrent modification exception in spread view 2024-05-15 17:18:03 -04:00
woodser
8462ff1019 document changing default application folder 2024-05-15 10:00:48 -04:00
woodser
1aa62863f4 move DEFAULT_APP_NAME to HavenoExecutable 2024-05-15 09:33:54 -04:00
woodser
7e3d89797e recover from failed payout tx 2024-05-15 09:33:54 -04:00
woodser
7847460f11 bump version to 1.0.4 2024-05-15 07:40:17 -04:00
woodser
aee7a539b5 remove old verification dependencies 2024-05-15 07:17:25 -04:00
nsec1
1b864368e1
Update p2p package #756 (#884) 2024-05-15 06:52:36 -04:00
woodser
a723c0d86b taker security deposit based on trade amount in range 2024-05-15 06:51:04 -04:00
woodser
281b7d0905 update documentation 2024-05-13 07:26:05 -04:00
woodser
816ba912a1 update documentation 2024-05-13 07:11:59 -04:00
woodser
6dedce1d27 do not cancel take offer screen on maker disconnect if initializing 2024-05-13 07:11:59 -04:00
woodser
091ffd98eb set trade wallet sync tolerance to 5 blocks 2024-05-13 07:11:59 -04:00
woodser
cc247bbc69 update documentation 2024-05-12 08:20:28 -04:00
woodser
0fed23ec22 bump version to 1.0.3 2024-05-12 08:01:51 -04:00
woodser
b81922cd9e update documentation 2024-05-12 07:59:02 -04:00
woodser
1bf83ecb8b update deployment guide 2024-05-11 11:20:47 -04:00
woodser
a9f7a06e1f make configurable if arbitrator assigns trade fee address 2024-05-11 11:20:47 -04:00
Jabster28
7aebc7bc31
chore: re-enable macos artifact uploads (#895) 2024-05-10 14:05:14 -04:00
woodser
3cdd88b569 support --ignoreLocalXmrNode startup flag 2024-05-09 18:39:55 -04:00
woodser
416761af41 update makefile seed ports 2024-05-09 18:39:55 -04:00
woodser
235c0682b3 update readme 2024-05-09 18:39:55 -04:00
Jabster28
3814b5b6db
chore: lock macos version back to x86 (#892)
* also add error reports
2024-05-09 18:03:00 -04:00
woodser
f4de560764 process payout tx if not confirmed, send deposit responses once 2024-05-08 18:08:25 -04:00
woodser
f1b8cd1e2e thaw reserved inputs and re-freeze offer inputs on create tx errors 2024-05-08 09:12:35 -04:00
woodser
5d7991e4f7 repeat refresh offer on TTL error 2024-05-08 09:12:35 -04:00
woodser
203386f03d wait to observe published tx on error with payment received 2024-05-08 09:12:35 -04:00
woodser
d64ee42154 skip processing payout tx if already published 2024-05-08 09:12:35 -04:00
woodser
adf7348515 bump reprocessing delay to 5s 2024-05-08 09:12:35 -04:00
woodser
0ea056104c support invalid offer state 2024-05-08 09:12:35 -04:00
woodser
7887c450c7 arbitrator sends deposit responses on error or timeout 2024-05-08 09:12:35 -04:00
woodser
a6d827c369 latch trade awaits trade initialization 2024-05-08 09:12:35 -04:00
woodser
4ec5339e5d release wallet lock processing payout tx 2024-05-08 09:12:35 -04:00
woodser
b179203dd2 repeat wallet attempts after 2s 2024-05-08 09:12:35 -04:00
woodser
4761b71105 remove repeated popups on offer taken error 2024-05-08 09:12:35 -04:00
woodser
399d4e0512 thaw outputs off main thread on cancel offer 2024-05-08 09:12:35 -04:00
woodser
6b34651101 stop busy animation on reset payment state 2024-05-07 21:37:49 -04:00
woodser
78ec06b851 add trade init steps and reset timeout 2024-05-07 21:37:49 -04:00
woodser
6fb846d783 refactor trade protocol error handling and wallet deletion 2024-05-07 21:37:49 -04:00
woodser
b034ac8c13 use consistent postfix for mainnet in makefile 2024-05-07 21:37:49 -04:00
woodser
78ae449e18 use dns seed nodes from MainNetParams on stagenet 2024-05-05 11:30:22 -04:00
woodser
f99fab8515 repeat try withdraw tx and fix amount details 2024-05-05 08:33:49 -04:00
woodser
ceff34672d recover from deleted wallet cache 2024-05-04 09:16:01 -04:00
woodser
b50238a805 log cleanup 2024-05-04 09:16:01 -04:00
woodser
f53a4e5fad show popup on error relaying withdraw tx 2024-05-04 09:16:01 -04:00
woodser
e96b875232 use more cached wallet state instead of direct queries 2024-05-04 09:16:01 -04:00
woodser
a5883d7bcd arbitrator assigns trade fee address 2024-05-04 09:16:01 -04:00
woodser
eefcf0191f trade step timeout is 60s on testnet 2024-04-30 15:34:51 -04:00
woodser
2341e73da2 defer dispute payout tx if awaiting peer ack or closed 2024-04-30 13:52:04 -04:00
woodser
f8480a1a4d remove unused import 2024-04-30 13:07:36 -04:00
woodser
e2f19c280e defer dispute payout tx if awaiting peer ack 2024-04-30 12:53:12 -04:00
woodser
467b678ea7 check if dispute payout published on submit failure, thread fixes 2024-04-30 11:18:04 -04:00
woodser
a3f2cd875c update balances on same thread 2024-04-30 11:18:04 -04:00
woodser
5531d4eea1 increase max tx attempts to 5 2024-04-29 11:22:49 -04:00
woodser
8e24ebfc23 import multisig hex locks on daemon due to refresh call 2024-04-29 11:22:49 -04:00
woodser
bd1be1041a increase timeout for sign offer request ack to 30s 2024-04-29 11:22:49 -04:00
woodser
7a0c8a3f3b bump version to 1.0.2 2024-04-29 11:22:49 -04:00
woodser
d1e5910502 non-mature currencies have chargeback risk too 2024-04-29 11:22:49 -04:00
woodser
7de2e9de6a increase max calls to resolve disputes for tests 2024-04-29 11:22:49 -04:00
woodser
94ab3c1f9b show reserved amount in maker's offer details 2024-04-29 11:22:49 -04:00
woodser
e63141279c refactoring based on congestion testing
retry creating and processing trade txs on failure
do not use connection manager polling to reduce requests
use global daemon lock for wallet sync operations
sync wallets on poll if behind
use local util to get payment uri to avoid blocking
all peers share multisig hex on deposits confirmed
import multisig hex when needed
2024-04-29 11:22:49 -04:00
woodser
f519ac12a5 update monero-java to 0.8.26 2024-04-29 11:22:49 -04:00
woodser
895acc9d7c do not fetch trade txs directly from daemon 2024-04-27 17:14:21 -04:00
woodser
2e672260d3 show '0 confirmation(s)' until confirmed 2024-04-27 17:14:21 -04:00
woodser
6455171dea disable payment sent/received buttons until acked 2024-04-22 04:56:49 -04:00
woodser
3a66c9cd24 import multisig hex off main thread on payment sent message 2024-04-22 04:56:49 -04:00
woodser
e4b80ef14b do not update from pool on shared wallet sync 2024-04-22 04:56:49 -04:00
woodser
d0a25d7d5b show 'preparing confirmation' on payment sent/received clicked 2024-04-22 04:56:49 -04:00
woodser
adccf27385 set deposit tx confirmations from wallet instead of daemon request 2024-04-22 04:56:49 -04:00
woodser
f0862b7aeb synchronize reserving funds for open offer to fix race condition 2024-04-22 04:56:49 -04:00
woodser
9d9635ff50 refactor wallet poll loops to further minimize requests 2024-04-22 04:56:49 -04:00
woodser
5c0d9a1ae5 remove timeout confirming payment sent/received & revert state on error 2024-04-21 06:27:30 -04:00
woodser
0ead6d8f83 adjust public and provided monero nodes 2024-04-21 06:27:30 -04:00
woodser
b08d6833a8 public nodes option entails provided nodes 2024-04-21 06:27:30 -04:00
woodser
8097b0f499 use cached txs in xmr wallet service instead of querying wallet 2024-04-21 06:27:30 -04:00
woodser
a107acbdb4 add more mainnet nodes for diversity and reliability 2024-04-18 14:19:52 -04:00
woodser
97d35dda33 bump version to 1.0.1 2024-04-18 14:19:52 -04:00
woodser
ca2d7704ab fixes from congestion testing
- refactor main wallet polling
- restart main wallet if connection changes before initial sync
- use cached wallet state throughout app
- avoid rescanning spent outputs until payout tx expected
- allow payment sent/received buttons to be clicked until arrived
- apply timeout to payment sent/received buttons
- load DepositView asynchronously
- remove separate timeout from OpenOffer
- tolerate error importing multisig hex until necessary
2024-04-18 14:19:52 -04:00
woodser
9cbf042da2 update to monero-java 0.8.24 2024-04-18 14:19:52 -04:00
woodser
f7ac9ae37a show sync progress before sync starts on startup 2024-04-13 10:28:50 -04:00
woodser
b86e916dcb invoke error handler once on trade failure 2024-04-13 10:28:50 -04:00
woodser
b6a113b742 trade appears after deposit published 2024-04-13 10:28:50 -04:00
woodser
9062bc9159 log error initializing main wallet 2024-04-13 10:28:50 -04:00
woodser
ccf2757418 increase trade protocol step timeout to 2m 2024-04-13 10:28:50 -04:00
woodser
2ba37d98fe shut down XmrWalletService with timeout 2024-04-13 10:28:50 -04:00
justynboyer@gmail.com
e629a8c63a chore: don't run codacy if on a fork 2024-04-13 03:57:26 -04:00
woodser
ae08caa287 document registering monero nodes in deployment guide 2024-04-12 09:33:30 -04:00
woodser
7d348febab add config to start mainnet application from Makefile 2024-04-12 09:33:30 -04:00
Jabster28
4a1a4f359e
combine artifacts and build workflow (#866) 2024-04-12 09:05:29 -04:00
Jabster28
de07a926c2
add installer builds to artifacts w/ GH action (#864) 2024-04-12 05:06:45 -04:00
524 changed files with 27935 additions and 9586 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -37,3 +37,5 @@ deploy
.vscode
.vim/*
*/.factorypath
.flatpak-builder
exchange.haveno.Haveno.yaml

View File

@ -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
View File

@ -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 \

View File

@ -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">
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/505405b43cb74d5a996f106a3371588e)](https://app.codacy.com/gh/haveno-dex/haveno/dashboard)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=project%3Ahaveno-dex%2F2)](https://github.com/orgs/haveno-dex/projects/2) |
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
[![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex)
[![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](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`
<!-- ![Qr code](https://raw.githubusercontent.com/haveno-dex/haveno/master/media/qrhaveno.png) -->
<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`
<!-- ![Qr code](https://raw.githubusercontent.com/haveno-dex/haveno/master/media/qrbtc.png) -->

View File

@ -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);
}

View File

@ -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."

View File

@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
0.00,
defaultBuyerSecurityDepositPct.get(),
defaultSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};

View File

@ -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());

View File

@ -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());

View File

@ -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));

View File

@ -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());

View File

@ -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);
}

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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());

View File

@ -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 {}.",

View File

@ -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;

View 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}"));
}
}

View File

@ -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

View File

@ -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");
}
}

View 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");
}
}

View 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");
}
}

View File

@ -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");
}
}

View File

@ -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

View File

@ -32,6 +32,7 @@ public class BitcoinTest extends AbstractAssetTest {
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
}
@Test

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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,

View File

@ -81,7 +81,7 @@ public class OffersServiceRequest {
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMarginPct(marketPriceMarginPct)
.setBuyerSecurityDepositPct(securityDepositPct)
.setSecurityDepositPct(securityDepositPct)
.setPaymentAccountId(paymentAcctId)
.setTriggerPrice(triggerPrice)
.build();

View File

@ -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));
}
}

View File

@ -47,6 +47,7 @@ public class ThreadUtils {
synchronized (THREADS) {
THREADS.put(threadId, Thread.currentThread());
}
Thread.currentThread().setName(threadId);
command.run();
});
}

View File

@ -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();
}
}
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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())

View File

@ -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);
}

View File

@ -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

View File

@ -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
}

View File

@ -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()));
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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());

View File

@ -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());

View File

@ -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))

View File

@ -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 +

View File

@ -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);
}

View File

@ -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);
});

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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"));

View File

@ -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 {

View File

@ -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() {

View File

@ -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;

View File

@ -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());
}

View File

@ -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");

View File

@ -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.");

View File

@ -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;

View File

@ -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 = "";

View File

@ -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));
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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();
}
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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 "";
}

View File

@ -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);
}
}

View File

@ -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);
// }
}
}

View File

@ -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;
}
}

View File

@ -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()),

View File

@ -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

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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());

View File

@ -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);

View File

@ -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