Compare commits

..

270 Commits

Author SHA1 Message Date
woodser
de5250e89a persist trade with payment confirmation msgs before processing 2025-04-22 08:16:13 -04:00
woodser
923b3ad73b do not await updating trade state properties on trade thread 2025-04-22 08:16:13 -04:00
XMRZombie
9a14d5552e
Update tails script to expect installer instead of archive
Update haveno-install.sh
Archive extraction bypass, also renaming the filename to package_filename trough mv for keeping install.sh stable
2025-04-21 11:11:29 -04:00
woodser
a3d3f51f02
change popup from warning to error on take offer error 2025-04-21 10:29:06 -04:00
woodser
77429472f4
fix error popup when arbitrator nacks signing offer 2025-04-21 09:26:49 -04:00
woodser
38615edf86
re-arrange deployment sections and use markup for notes & warnings 2025-04-21 09:02:17 -04:00
woodser
cf9a37f295
improve error handling when clones taken at the same time 2025-04-19 22:28:32 -04:00
woodser
c7a3a9740f
fixes when cloned offers are taken at the same time 2025-04-19 16:54:01 -04:00
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
323 changed files with 13602 additions and 4606 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 name: CI
on: on:
@ -26,21 +29,23 @@ jobs:
cache: gradle cache: gradle
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build --stacktrace --scan run: ./gradlew build --stacktrace --scan
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: error-reports-${{ matrix.os }} name: error-reports-${{ matrix.os }}
path: ${{ github.workspace }}/desktop/build/reports path: ${{ github.workspace }}/desktop/build/reports
- name: cache nodes dependencies - name: cache nodes dependencies
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
include-hidden-files: true
name: cached-localnet name: cached-localnet
path: .localnet path: .localnet
overwrite: true
- name: Install dependencies - name: Install dependencies
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
run: | run: |
sudo apt update sudo apt-get update
sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream 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 flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Install WiX Toolset - name: Install WiX Toolset
if: ${{ matrix.os == 'windows-latest' }} if: ${{ matrix.os == 'windows-latest' }}
@ -67,10 +72,9 @@ jobs:
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
shell: powershell shell: powershell
- name: Move Release Files on Unix - name: Move Release Files for Linux
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
run: | run: |
if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then
mkdir ${{ github.workspace }}/release-linux-rpm mkdir ${{ github.workspace }}/release-linux-rpm
mkdir ${{ github.workspace }}/release-linux-deb mkdir ${{ github.workspace }}/release-linux-deb
mkdir ${{ github.workspace }}/release-linux-flatpak mkdir ${{ github.workspace }}/release-linux-flatpak
@ -83,58 +87,87 @@ jobs:
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-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-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 }}/release-linux-flatpak
else 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 mkdir ${{ github.workspace }}/release-macos
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg 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 }}/release-macos
fi cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
shell: bash shell: bash
- name: Move Release Files on Windows - name: Move Release Files on Windows
if: ${{ matrix.os == 'windows-latest' }} if: ${{ matrix.os == 'windows-latest' }}
run: | run: |
mkdir ${{ github.workspace }}/release-windows 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 Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows 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 shell: powershell
# win # win
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: "Windows artifacts" name: "Windows artifacts"
if: ${{ matrix.os == 'windows-latest'}} if: ${{ matrix.os == 'windows-latest' }}
with: with:
name: haveno-windows name: haveno-windows
path: ${{ github.workspace }}/release-windows path: ${{ github.workspace }}/release-windows
# macos # macos
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: "macOS artifacts" name: "macOS artifacts"
if: ${{ matrix.os == 'macos-13' }} if: ${{ matrix.os == 'macos-13' }}
with: with:
name: haveno-macos name: haveno-macos
path: ${{ github.workspace }}/release-macos path: ${{ github.workspace }}/release-macos
# linux # linux
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: "Linux - deb artifact" name: "Linux - deb artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
with: with:
name: haveno-linux-deb name: haveno-linux-deb
path: ${{ github.workspace }}/release-linux-deb path: ${{ github.workspace }}/release-linux-deb
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: "Linux - rpm artifact" name: "Linux - rpm artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
with: with:
name: haveno-linux-rpm name: haveno-linux-rpm
path: ${{ github.workspace }}/release-linux-rpm path: ${{ github.workspace }}/release-linux-rpm
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: "Linux - AppImage artifact" name: "Linux - AppImage artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
with: with:
name: haveno-linux-appimage name: haveno-linux-appimage
path: ${{ github.workspace }}/release-linux-appimage path: ${{ github.workspace }}/release-linux-appimage
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
name: "Linux - flatpak artifact" name: "Linux - flatpak artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: ${{ matrix.os == 'ubuntu-22.04' }}
with: with:
name: haveno-linux-flatpak name: haveno-linux-flatpak
path: ${{ github.workspace }}/release-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

@ -9,7 +9,7 @@ jobs:
build: build:
if: github.repository == 'haveno-dex/haveno' if: github.repository == 'haveno-dex/haveno'
name: Publish coverage name: Publish coverage
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -18,7 +18,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-22.04
permissions: permissions:
actions: read actions: read
contents: read contents: read
@ -44,7 +44,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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 run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@ -7,7 +7,7 @@ on:
jobs: jobs:
issueLabeled: issueLabeled:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Bounty explanation - name: Bounty explanation
uses: peter-evans/create-or-update-comment@v3 uses: peter-evans/create-or-update-comment@v3

View File

@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 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 Copyright (C) 2020 Haveno Dex
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. 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. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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. 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, 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. 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 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/>.

View File

@ -70,9 +70,11 @@ monerod1-local:
--log-level 0 \ --log-level 0 \
--add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:48080 \
--add-exclusive-node 127.0.0.1:58080 \ --add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \ --rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \ --fixed-difficulty 500 \
--disable-rpc-ban \ --disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
monerod2-local: monerod2-local:
./.localnet/monerod \ ./.localnet/monerod \
@ -88,9 +90,11 @@ monerod2-local:
--confirm-external-bind \ --confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:58080 \ --add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \ --rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \ --fixed-difficulty 500 \
--disable-rpc-ban \ --disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
monerod3-local: monerod3-local:
./.localnet/monerod \ ./.localnet/monerod \
@ -106,9 +110,11 @@ monerod3-local:
--confirm-external-bind \ --confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:48080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \ --rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \ --fixed-difficulty 500 \
--disable-rpc-ban \ --disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
#--proxy 127.0.0.1:49775 \ #--proxy 127.0.0.1:49775 \
@ -417,6 +423,17 @@ haveno-desktop-stagenet:
--apiPort=3204 \ --apiPort=3204 \
--useNativeXmrWallet=false \ --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 # Mainnet network
monerod: monerod:
@ -468,6 +485,31 @@ arbitrator-desktop-mainnet:
--xmrNode=http://127.0.0.1:18081 \ --xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \ --useNativeXmrWallet=false \
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-mainnet:
./haveno-daemon$(APP_EXT) \ ./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \ --baseCurrencyNetwork=XMR_MAINNET \

View File

@ -23,13 +23,22 @@ Main features:
See the [FAQ on our website](https://haveno.exchange/faq/) for more information. See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
## Haveno Demo
https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47
## Installing Haveno ## Installing Haveno
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. We do not endorse any networks at this time. 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. 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 [start your own network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). 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. Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
@ -47,7 +56,7 @@ If you wish to help, take a look at the repositories above and look for open iss
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: 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`) - 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 Email: contact@haveno.exchange
Website: [haveno.exchange](https://haveno.exchange) Website: [haveno.exchange](https://haveno.exchange)
@ -58,7 +67,7 @@ See the [developer guide](docs/developer-guide.md) to get started developing for
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. 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 ## Bounties
@ -68,18 +77,9 @@ To incentivize development and reward contributors, we adopt a simple bounty sys
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: 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
<p> <p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br> <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> <code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
</p> </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. 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
<p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="115" height="115"><br>
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
</p>

View File

@ -43,7 +43,7 @@ import java.util.stream.Collectors;
import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; 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.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream; 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); return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
} }
public static final Supplier<Double> defaultBuyerSecurityDepositPct = () -> { public static final Supplier<Double> defaultSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent()); var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
if (defaultPct.precision() != 2) if (defaultPct.precision() != 2)
throw new IllegalStateException(format( throw new IllegalStateException(format(
"Unexpected decimal precision, expected 2 but actual is %d%n." "Unexpected decimal precision, expected 2 but actual is %d%n."

View File

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

View File

@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
"36000", "36000",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
audAccount.getId()); audAccount.getId());
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());
@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
"30000.1234", "30000.1234",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
usdAccount.getId()); usdAccount.getId());
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());
@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
"29500.1234", "29500.1234",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
eurAccount.getId()); eurAccount.getId());
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());

View File

@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
usdAccount.getId(), usdAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
10_000_000L, 10_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
nzdAccount.getId(), nzdAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
gbpAccount.getId(), gbpAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
brlAccount.getId(), brlAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer)); log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L, 10_000_000L,
5_000_000L, 5_000_000L,
0.0, 0.0,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
usdAccount.getId(), usdAccount.getId(),
triggerPrice); triggerPrice);
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());

View File

@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L, 100_000_000L,
75_000_000L, 75_000_000L,
"0.005", // FIXED PRICE IN BTC FOR 1 XMR "0.005", // FIXED PRICE IN BTC FOR 1 XMR
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId()); alicesXmrAcct.getId());
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());
@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L, 100_000_000L,
50_000_000L, 50_000_000L,
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId()); alicesXmrAcct.getId());
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer()); assertTrue(newOffer.getIsMyOffer());
@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L, 100_000_000L,
75_000_000L, 75_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(), alicesXmrAcct.getId(),
triggerPrice); triggerPrice);
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L, 100_000_000L,
50_000_000L, 50_000_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(), alicesXmrAcct.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); 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, // exceeds amount limit
100000000000L, 100000000000L,
"10000.0000", "10000.0000",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
usdAccount.getId())); usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
} }
@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L, 10000000L,
10000000L, 10000000L,
"40000.0000", "40000.0000",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
chfAccount.getId())); chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage()); assertEquals(expectedError, exception.getMessage());
@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L, 10000000L,
10000000L, 10000000L,
"63000.0000", "63000.0000",
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
audAccount.getId())); audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage()); assertEquals(expectedError, exception.getMessage());

View File

@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
12_500_000L, 12_500_000L,
12_500_000L, // min-amount = amount 12_500_000L, // min-amount = amount
0.00, 0.00,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(), alicesUsdAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();

View File

@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
1_000_000L, 1_000_000L,
1_000_000L, // min-amount = amount 1_000_000L, // min-amount = amount
0.00, 0.00,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesPaymentAccount.getId(), alicesPaymentAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();

View File

@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
15_000_000L, 15_000_000L,
7_500_000L, 7_500_000L,
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR "0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId()); alicesXmrAcct.getId());
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 5000); genBtcBlocksThenWait(1, 5000);

View File

@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
12_500_000L, 12_500_000L,
12_500_000L, // min-amount = amount 12_500_000L, // min-amount = amount
0.00, 0.00,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(), alicesUsdAccount.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();

View File

@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
20_000_000L, 20_000_000L,
10_500_000L, 10_500_000L,
priceMarginPctInput, priceMarginPctInput,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(), alicesXmrAcct.getId(),
NO_TRIGGER_PRICE); NO_TRIGGER_PRICE);
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); 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,
1_000_000, 1_000_000,
0.00, 0.00,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
paymentAcct.getId(), paymentAcct.getId(),
triggerPrice); triggerPrice);
log.info("SELL offer {} created with margin based price {}.", log.info("SELL offer {} created with margin based price {}.",
@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
1_000_000, 1_000_000,
1_000_000, 1_000_000,
0.00, 0.00,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
paymentAcct.getId(), paymentAcct.getId(),
triggerPrice); triggerPrice);
log.info("BUY offer {} created with margin based price {}.", 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.Objects;
import java.util.function.Supplier; 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.formatInternalFiatPrice;
import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.cli.CurrencyFormat.formatSatoshis;
import static haveno.common.util.MathUtils.scaleDownByPowerOf10; import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
@ -119,7 +119,7 @@ public class RandomOffer {
amount, amount,
minAmount, minAmount,
priceMargin, priceMargin,
defaultBuyerSecurityDepositPct.get(), defaultSecurityDepositPct.get(),
"0" /*no trigger price*/); "0" /*no trigger price*/);
} else { } else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount, this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
@ -128,7 +128,7 @@ public class RandomOffer {
amount, amount,
minAmount, minAmount,
fixedOfferPrice, fixedOfferPrice,
defaultBuyerSecurityDepositPct.get()); defaultSecurityDepositPct.get());
} }
this.id = offer.getId(); this.id = offer.getId();
return this; return this;

View File

@ -21,7 +21,7 @@
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete * {@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 * 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.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
* {@link haveno.asset.tokens.DaiStablecoin}. * {@link haveno.asset.tokens.DaiStablecoinERC20}.
* <p> * <p>
* The purpose of this package is to provide everything necessary for registering * 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 * ("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; import haveno.asset.Erc20Token;
public class USDCoin extends Erc20Token { public class DaiStablecoinERC20 extends Erc20Token {
public USDCoin() { public DaiStablecoinERC20() {
super("USD Coin", "USDC"); super("Dai Stablecoin", "DAI-ERC20");
} }
} }

View File

@ -19,9 +19,9 @@ package haveno.asset.tokens;
import haveno.asset.Erc20Token; import haveno.asset.Erc20Token;
public class DaiStablecoin extends Erc20Token { public class USDCoinERC20 extends Erc20Token {
public DaiStablecoin() { public USDCoinERC20() {
super("Dai Stablecoin", "DAI"); super("USD Coin (ERC20)", "USDC-ERC20");
} }
} }

View File

@ -9,3 +9,5 @@ haveno.asset.coins.Litecoin
haveno.asset.coins.Monero haveno.asset.coins.Monero
haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20 haveno.asset.tokens.TetherUSDTRC20
haveno.asset.tokens.USDCoinERC20
haveno.asset.tokens.DaiStablecoinERC20

View File

@ -49,7 +49,7 @@ configure(subprojects) {
gsonVersion = '2.8.5' gsonVersion = '2.8.5'
guavaVersion = '32.1.1-jre' guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0' guiceVersion = '7.0.0'
moneroJavaVersion = '0.8.33' moneroJavaVersion = '0.8.36'
httpclient5Version = '5.0' httpclient5Version = '5.0'
hamcrestVersion = '2.2' hamcrestVersion = '2.2'
httpclientVersion = '4.5.12' httpclientVersion = '4.5.12'
@ -71,7 +71,7 @@ configure(subprojects) {
loggingVersion = '1.2' loggingVersion = '1.2'
lombokVersion = '1.18.30' lombokVersion = '1.18.30'
mockitoVersion = '5.10.0' mockitoVersion = '5.10.0'
netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11 netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
protobufVersion = '3.19.1' protobufVersion = '3.19.1'
protocVersion = protobufVersion protocVersion = protobufVersion
pushyVersion = '0.13.2' pushyVersion = '0.13.2'
@ -457,14 +457,14 @@ configure(project(':core')) {
doLast { doLast {
// get monero binaries download url // get monero binaries download url
Map moneroBinaries = [ Map moneroBinaries = [
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-x86_64.tar.gz', 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68', 'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz', 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de', 'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz', 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287', 'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip', 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888' 'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
] ]
String osKey String osKey
@ -506,6 +506,7 @@ configure(project(':core')) {
} else { } else {
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir) ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
} }
}
// add the current platform's monero dependencies into the resources folder for installation // add the current platform's monero dependencies into the resources folder for installation
copy { copy {
@ -517,7 +518,6 @@ configure(project(':core')) {
into "${project(':core').projectDir}/src/main/resources/bin" into "${project(':core').projectDir}/src/main/resources/bin"
} }
} }
}
ext.extractArchiveTarGz = { File tarGzFile, File destinationDir -> ext.extractArchiveTarGz = { File tarGzFile, File destinationDir ->
println "Extracting tar.gz ${tarGzFile}" println "Extracting tar.gz ${tarGzFile}"
@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle' apply from: 'package/package.gradle'
version = '1.0.13-SNAPSHOT' version = '1.1.0-SNAPSHOT'
jar.manifest.attributes( jar.manifest.attributes(
"Implementation-Title": project.name, "Implementation-Title": project.name,

View File

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

View File

@ -69,7 +69,7 @@ public class ClockWatcher {
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { 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)); listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
} }
} }

View File

@ -59,10 +59,12 @@ public class Capabilities {
} }
public Capabilities(Collection<Capability> capabilities) { public Capabilities(Collection<Capability> capabilities) {
synchronized (capabilities) {
synchronized (this.capabilities) { synchronized (this.capabilities) {
this.capabilities.addAll(capabilities); this.capabilities.addAll(capabilities);
} }
} }
}
public void set(Capability... capabilities) { public void set(Capability... capabilities) {
set(Arrays.asList(capabilities)); set(Arrays.asList(capabilities));
@ -73,11 +75,13 @@ public class Capabilities {
} }
public void set(Collection<Capability> capabilities) { public void set(Collection<Capability> capabilities) {
synchronized (capabilities) {
synchronized (this.capabilities) { synchronized (this.capabilities) {
this.capabilities.clear(); this.capabilities.clear();
this.capabilities.addAll(capabilities); this.capabilities.addAll(capabilities);
} }
} }
}
public void addAll(Capability... capabilities) { public void addAll(Capability... capabilities) {
synchronized (this.capabilities) { synchronized (this.capabilities) {
@ -87,17 +91,21 @@ public class Capabilities {
public void addAll(Capabilities capabilities) { public void addAll(Capabilities capabilities) {
if (capabilities != null) { if (capabilities != null) {
synchronized (capabilities.capabilities) {
synchronized (this.capabilities) { synchronized (this.capabilities) {
this.capabilities.addAll(capabilities.capabilities); this.capabilities.addAll(capabilities.capabilities);
} }
} }
} }
}
public boolean containsAll(final Set<Capability> requiredItems) { public boolean containsAll(final Set<Capability> requiredItems) {
synchronized(requiredItems) {
synchronized (this.capabilities) { synchronized (this.capabilities) {
return capabilities.containsAll(requiredItems); return capabilities.containsAll(requiredItems);
} }
} }
}
public boolean containsAll(final Capabilities capabilities) { public boolean containsAll(final Capabilities capabilities) {
return containsAll(capabilities.capabilities); return containsAll(capabilities.capabilities);
@ -129,8 +137,10 @@ public class Capabilities {
* @return int list of Capability ordinals * @return int list of Capability ordinals
*/ */
public static List<Integer> toIntList(Capabilities capabilities) { public static List<Integer> toIntList(Capabilities capabilities) {
synchronized (capabilities.capabilities) {
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
} }
}
/** /**
* helper for protobuffer stuff * helper for protobuffer stuff
@ -139,12 +149,14 @@ public class Capabilities {
* @return a {@link Capabilities} object * @return a {@link Capabilities} object
*/ */
public static Capabilities fromIntList(List<Integer> capabilities) { public static Capabilities fromIntList(List<Integer> capabilities) {
synchronized (capabilities) {
return new Capabilities(capabilities.stream() return new Capabilities(capabilities.stream()
.filter(integer -> integer < Capability.values().length) .filter(integer -> integer < Capability.values().length)
.filter(integer -> integer >= 0) .filter(integer -> integer >= 0)
.map(integer -> Capability.values()[integer]) .map(integer -> Capability.values()[integer])
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
}
/** /**
* *
@ -181,8 +193,10 @@ public class Capabilities {
} }
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
synchronized (capabilities.capabilities) {
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
} }
}
@Override @Override
public String toString() { public String toString() {
@ -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 // 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. // backward compatibility issues, so we should treat capabilities as an append-only data structure.
public int findHighestCapability(Capabilities capabilities) { public int findHighestCapability(Capabilities capabilities) {
synchronized (capabilities.capabilities) {
return (int) capabilities.capabilities.stream() return (int) capabilities.capabilities.stream()
.mapToLong(e -> (long) e.ordinal()) .mapToLong(e -> (long) e.ordinal())
.sum(); .sum();
} }
}
} }

View File

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version { public class Version {
// The application versions // The application versions
// We use semantic versioning with major, minor and patch // We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.13"; public static final String VERSION = "1.1.0";
/** /**
* Holds a list of the tagged resource files for optimizing the getData requests. * Holds a list of the tagged resource files for optimizing the getData requests.
@ -72,6 +72,25 @@ public class Version {
return false; 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) { private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\."); final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version); 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 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 // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app. // the Haveno app.
// VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
public static final int TRADE_PROTOCOL_VERSION = 1; // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2;
private static String p2pMessageVersion; private static String p2pMessageVersion;
public static String getP2PMessageVersion() { public static String getP2PMessageVersion() {

View File

@ -117,6 +117,8 @@ public class Config {
public static final String BTC_FEE_INFO = "bitcoinFeeInfo"; public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String PASSWORD_REQUIRED = "passwordRequired"; 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 // Default values for certain options
public static final int UNSPECIFIED_PORT = -1; public static final int UNSPECIFIED_PORT = -1;
@ -204,6 +206,8 @@ public class Config {
public final boolean republishMailboxEntries; public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation; public final boolean bypassMempoolValidation;
public final boolean passwordRequired; public final boolean passwordRequired;
public final boolean updateXmrBinaries;
public final String xmrBlockchainPath;
// Properties derived from options but not exposed as options themselves // Properties derived from options but not exposed as options themselves
public final File torDir; public final File torDir;
@ -621,6 +625,20 @@ public class Config {
.ofType(boolean.class) .ofType(boolean.class)
.defaultsTo(false); .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 { try {
CompositeOptionSet options = new CompositeOptionSet(); CompositeOptionSet options = new CompositeOptionSet();
@ -733,6 +751,8 @@ public class Config {
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.passwordRequired = options.valueOf(passwordRequiredOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt);
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
} catch (OptionException ex) { } catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s", throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0), ex.options().get(0),
@ -742,11 +762,11 @@ public class Config {
} }
// Create all appDataDir subdirectories and assign to their respective properties // Create all appDataDir subdirectories and assign to their respective properties
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(btcNetworkDir, "keys"); this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
this.storageDir = mkdir(btcNetworkDir, "db"); this.storageDir = mkdir(xmrNetworkDir, "db");
this.torDir = mkdir(btcNetworkDir, "tor"); this.torDir = mkdir(xmrNetworkDir, "tor");
this.walletDir = mkdir(btcNetworkDir, "wallet"); this.walletDir = mkdir(xmrNetworkDir, "wallet");
// Assign values to special-case static fields // Assign values to special-case static fields
APP_DATA_DIR_VALUE = appDataDir; 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. * @param password The password to unlock the keys or to generate new keys, nullable.
*/ */
public void generateKeys(String password) { 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); symmetricKey = Encryption.generateSecretKey(256);
signatureKeyPair = Sig.generateKeyPair(); signatureKeyPair = Sig.generateKeyPair();
encryptionKeyPair = Encryption.generateKeyPair(); encryptionKeyPair = Encryption.generateKeyPair();

View File

@ -243,6 +243,11 @@ public class KeyStorage {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
storageDir.mkdirs(); 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 oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
var passwordChars = password == null ? new char[0] : password.toCharArray(); var passwordChars = password == null ? new char[0] : password.toCharArray();
try { try {

View File

@ -32,6 +32,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
@ -76,11 +77,11 @@ public class FileUtil {
public static List<File> getBackupFiles(File dir, String fileName) { public static List<File> getBackupFiles(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
if (!backupDir.exists()) return null; if (!backupDir.exists()) return new ArrayList<File>();
String dirName = "backups_" + fileName; String dirName = "backups_" + fileName;
if (dirName.contains(".")) dirName = dirName.replace(".", "_"); if (dirName.contains(".")) dirName = dirName.replace(".", "_");
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
if (!backupFileDir.exists()) return null; if (!backupFileDir.exists()) return new ArrayList<File>();
File[] files = backupFileDir.listFiles(); File[] files = backupFileDir.listFiles();
return Arrays.asList(files); return Arrays.asList(files);
} }

View File

@ -335,12 +335,13 @@ public class SignedWitnessService {
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash()); String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8); String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey()); 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); key.verifyMessage(message, signatureBase64);
verifySignatureWithECKeyResultCache.put(hash, true); verifySignatureWithECKeyResultCache.put(hash, true);
return true; return true;
} else { } 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); verifySignatureWithECKeyResultCache.put(hash, false);
return false; return false;
} }

View File

@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferRestrictions; import haveno.core.offer.OfferRestrictions;
import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.ChargeBackRisk;
import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccount;
import haveno.core.payment.TradeLimits;
import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PaymentMethod;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
@ -498,10 +499,15 @@ public class AccountAgeWitnessService {
return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); 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) if (paymentAccount == null)
return 0; return 0;
if (buyerAsTakerWithoutDeposit) {
TradeLimits tradeLimits = new TradeLimits();
return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
}
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode); BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
if (hasTradeLimitException(accountAgeWitness)) { if (hasTradeLimitException(accountAgeWitness)) {

View File

@ -239,8 +239,8 @@ public class CoreApi {
xmrConnectionService.stopCheckingConnection(); xmrConnectionService.stopCheckingConnection();
} }
public MoneroRpcConnection getBestAvailableXmrConnection() { public MoneroRpcConnection getBestXmrConnection() {
return xmrConnectionService.getBestAvailableConnection(); return xmrConnectionService.getBestConnection();
} }
public void setXmrConnectionAutoSwitch(boolean autoSwitch) { public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
@ -419,10 +419,14 @@ public class CoreApi {
double marketPriceMargin, double marketPriceMargin,
long amountAsLong, long amountAsLong,
long minAmountAsLong, long minAmountAsLong,
double buyerSecurityDeposit, double securityDepositPct,
String triggerPriceAsString, String triggerPriceAsString,
boolean reserveExactAmount, boolean reserveExactAmount,
String paymentAccountId, String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler, Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
coreOffersService.postOffer(currencyCode, coreOffersService.postOffer(currencyCode,
@ -432,10 +436,14 @@ public class CoreApi {
marketPriceMargin, marketPriceMargin,
amountAsLong, amountAsLong,
minAmountAsLong, minAmountAsLong,
buyerSecurityDeposit, securityDepositPct,
triggerPriceAsString, triggerPriceAsString,
reserveExactAmount, reserveExactAmount,
paymentAccountId, paymentAccountId,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo,
sourceOfferId,
resultHandler, resultHandler,
errorMessageHandler); errorMessageHandler);
} }
@ -448,8 +456,11 @@ public class CoreApi {
double marketPriceMargin, double marketPriceMargin,
BigInteger amount, BigInteger amount,
BigInteger minAmount, BigInteger minAmount,
double buyerSecurityDeposit, double securityDepositPct,
PaymentAccount paymentAccount) { PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return coreOffersService.editOffer(offerId, return coreOffersService.editOffer(offerId,
currencyCode, currencyCode,
direction, direction,
@ -458,8 +469,11 @@ public class CoreApi {
marketPriceMargin, marketPriceMargin,
amount, amount,
minAmount, minAmount,
buyerSecurityDeposit, securityDepositPct,
paymentAccount); paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
} }
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -535,9 +549,11 @@ public class CoreApi {
public void takeOffer(String offerId, public void takeOffer(String offerId,
String paymentAccountId, String paymentAccountId,
long amountAsLong, long amountAsLong,
String challenge,
Consumer<Trade> resultHandler, Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId); Offer offer = coreOffersService.getOffer(offerId);
offer.setChallenge(challenge);
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler); coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
} }

View File

@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class CoreDisputesService { public class CoreDisputesService {
public enum DisputePayout { // TODO: persist in DisputeResult?
public enum PayoutSuggestion {
BUYER_GETS_TRADE_AMOUNT, BUYER_GETS_TRADE_AMOUNT,
BUYER_GETS_ALL, // used in desktop BUYER_GETS_ALL,
SELLER_GETS_TRADE_AMOUNT, SELLER_GETS_TRADE_AMOUNT,
SELLER_GETS_ALL, // used in desktop SELLER_GETS_ALL,
CUSTOM CUSTOM
} }
@ -172,17 +173,17 @@ public class CoreDisputesService {
// create dispute result // create dispute result
var closeDate = new Date(); var closeDate = new Date();
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
DisputePayout payout; PayoutSuggestion payoutSuggestion;
if (customWinnerAmount > 0) { if (customWinnerAmount > 0) {
payout = DisputePayout.CUSTOM; payoutSuggestion = PayoutSuggestion.CUSTOM;
} else if (winner == DisputeResult.Winner.BUYER) { } else if (winner == DisputeResult.Winner.BUYER) {
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
} else if (winner == DisputeResult.Winner.SELLER) { } else if (winner == DisputeResult.Winner.SELLER) {
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
} else { } else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
} }
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
// close winning dispute ticket // close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
@ -227,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 * 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. * 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(); Contract contract = dispute.getContract();
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
BigInteger tradeAmount = contract.getTradeAmount(); BigInteger tradeAmount = contract.getTradeAmount();
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); 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.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit); 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.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
} else if (payout == DisputePayout.SELLER_GETS_ALL) { } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); 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"); 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(); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative"); if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");

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.roundDoubleToLong;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import haveno.core.locale.CurrencyUtil; import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.CryptoMoney;
import haveno.core.monetary.Price; import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.TraditionalMoney;
@ -66,9 +67,7 @@ import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -124,7 +123,6 @@ public class CoreOffersService {
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers; return offers;
} }
@ -143,12 +141,9 @@ public class CoreOffersService {
} }
List<OpenOffer> getMyOffers() { List<OpenOffer> getMyOffers() {
List<OpenOffer> offers = openOfferManager.getOpenOffers().stream() return openOfferManager.getOpenOffers().stream()
.filter(o -> o.getOffer().isMyOffer(keyRing)) .filter(o -> o.getOffer().isMyOffer(keyRing))
.collect(Collectors.toList()); .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) { List<OpenOffer> getMyOffers(String direction, String currencyCode) {
@ -159,7 +154,7 @@ public class CoreOffersService {
} }
OpenOffer getMyOffer(String id) { OpenOffer getMyOffer(String id) {
return openOfferManager.getOpenOfferById(id) return openOfferManager.getOpenOffer(id)
.filter(open -> open.getOffer().isMyOffer(keyRing)) .filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() -> .orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id))); new IllegalStateException(format("openoffer with id '%s' not found", id)));
@ -172,19 +167,38 @@ public class CoreOffersService {
double marketPriceMargin, double marketPriceMargin,
long amountAsLong, long amountAsLong,
long minAmountAsLong, long minAmountAsLong,
double securityDeposit, double securityDepositPct,
String triggerPriceAsString, String triggerPriceAsString,
boolean reserveExactAmount, boolean reserveExactAmount,
String paymentAccountId, String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler, Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked(); coreWalletsService.verifyEncryptedWalletIsUnlocked();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
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 upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId(); String offerId = createOfferService.getRandomOfferId();
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase()); OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
@ -199,22 +213,78 @@ public class CoreOffersService {
price, price,
useMarketBasedPrice, useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01), exactMultiply(marketPriceMargin, 0.01),
securityDeposit, securityDepositPct,
paymentAccount); paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// We don't support atm funding from external wallet to keep it simple.
boolean useSavingsWallet = true;
//noinspection ConstantConditions
placeOffer(offer, placeOffer(offer,
triggerPriceAsString, triggerPriceAsString,
useSavingsWallet, true,
reserveExactAmount, reserveExactAmount,
null,
transaction -> resultHandler.accept(offer), transaction -> resultHandler.accept(offer),
errorMessageHandler); 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, Offer editOffer(String offerId,
String currencyCode, String currencyCode,
OfferDirection direction, OfferDirection direction,
@ -223,8 +293,11 @@ public class CoreOffersService {
double marketPriceMargin, double marketPriceMargin,
BigInteger amount, BigInteger amount,
BigInteger minAmount, BigInteger minAmount,
double buyerSecurityDeposit, double securityDepositPct,
PaymentAccount paymentAccount) { PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return createOfferService.createAndGetOffer(offerId, return createOfferService.createAndGetOffer(offerId,
direction, direction,
currencyCode.toUpperCase(), currencyCode.toUpperCase(),
@ -233,8 +306,11 @@ public class CoreOffersService {
price, price,
useMarketBasedPrice, useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01), exactMultiply(marketPriceMargin, 0.01),
buyerSecurityDeposit, securityDepositPct,
paymentAccount); paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
} }
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -244,26 +320,6 @@ public class CoreOffersService {
// -------------------------- PRIVATE HELPERS ----------------------------- // -------------------------- 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) { private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s", String error = format("cannot create %s offer with payment account %s",
@ -277,6 +333,7 @@ public class CoreOffersService {
String triggerPriceAsString, String triggerPriceAsString,
boolean useSavingsWallet, boolean useSavingsWallet,
boolean reserveExactAmount, boolean reserveExactAmount,
String sourceOfferId,
Consumer<Transaction> resultHandler, Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode()); long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
@ -285,6 +342,7 @@ public class CoreOffersService {
triggerPriceAsLong, triggerPriceAsLong,
reserveExactAmount, reserveExactAmount,
true, true,
sourceOfferId,
resultHandler::accept, resultHandler::accept,
errorMessageHandler); errorMessageHandler);
} }

View File

@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage;
import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TradeChatSession;
import haveno.core.support.traderchat.TraderChatManager; import haveno.core.support.traderchat.TraderChatManager;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Tradable;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeUtil; import haveno.core.trade.TradeUtil;
@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol;
import haveno.core.trade.protocol.SellerProtocol; import haveno.core.trade.protocol.SellerProtocol;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil; 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 haveno.core.xmr.wallet.BtcWalletService;
import static java.lang.String.format; import static java.lang.String.format;
import java.math.BigInteger; import java.math.BigInteger;
@ -68,7 +64,6 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bitcoinj.core.Coin;
@Singleton @Singleton
@Slf4j @Slf4j
@ -84,7 +79,6 @@ class CoreTradesService {
private final TakeOfferModel takeOfferModel; private final TakeOfferModel takeOfferModel;
private final TradeManager tradeManager; private final TradeManager tradeManager;
private final TraderChatManager traderChatManager; private final TraderChatManager traderChatManager;
private final TradeUtil tradeUtil;
private final OfferUtil offerUtil; private final OfferUtil offerUtil;
private final User user; private final User user;
@ -106,7 +100,6 @@ class CoreTradesService {
this.takeOfferModel = takeOfferModel; this.takeOfferModel = takeOfferModel;
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.traderChatManager = traderChatManager; this.traderChatManager = traderChatManager;
this.tradeUtil = tradeUtil;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.user = user; this.user = user;
} }
@ -132,7 +125,7 @@ class CoreTradesService {
// adjust amount for fixed-price offer (based on TakeOfferViewModel) // adjust amount for fixed-price offer (based on TakeOfferViewModel)
String currencyCode = offer.getCurrencyCode(); String currencyCode = offer.getCurrencyCode();
OfferDirection direction = offer.getOfferPayload().getDirection(); 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 (offer.getPrice() != null) {
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
@ -206,7 +199,7 @@ class CoreTradesService {
String getTradeRole(String tradeId) { String getTradeRole(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked(); coreWalletsService.verifyEncryptedWalletIsUnlocked();
return tradeUtil.getRole(getTrade(tradeId)); return TradeUtil.getRole(getTrade(tradeId));
} }
Trade getTrade(String tradeId) { Trade getTrade(String tradeId) {
@ -223,8 +216,7 @@ class CoreTradesService {
} }
private Optional<Trade> getClosedTrade(String tradeId) { private Optional<Trade> getClosedTrade(String tradeId) {
Optional<Tradable> tradable = closedTradableManager.getTradeById(tradeId); return closedTradableManager.getTradeById(tradeId);
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
} }
List<Trade> getTrades() { List<Trade> getTrades() {
@ -267,40 +259,9 @@ class CoreTradesService {
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; 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. // Throws a RuntimeException trade is already closed.
private void verifyTradeIsNotClosed(String tradeId) { private void verifyTradeIsNotClosed(String tradeId) {
if (getClosedTrade(tradeId).isPresent()) if (getClosedTrade(tradeId).isPresent())
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId)); 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

@ -32,6 +32,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.setup.DownloadListener; import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.Socks5ProxyProvider; import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.P2PServiceListener;
@ -40,7 +41,6 @@ import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
@ -64,7 +64,6 @@ import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper; import monero.common.TaskLooper;
import monero.daemon.MoneroDaemonRpc; import monero.daemon.MoneroDaemonRpc;
import monero.daemon.model.MoneroDaemonInfo; import monero.daemon.model.MoneroDaemonInfo;
import monero.daemon.model.MoneroPeer;
@Slf4j @Slf4j
@Singleton @Singleton
@ -73,6 +72,14 @@ public final class XmrConnectionService {
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet 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_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 REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
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 lock = new Object();
private final Object pollLock = new Object(); private final Object pollLock = new Object();
@ -85,12 +92,14 @@ public final class XmrConnectionService {
private final XmrLocalNode xmrLocalNode; private final XmrLocalNode xmrLocalNode;
private final MoneroConnectionManager connectionManager; private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList; 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 ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
private final LongProperty chainHeight = new SimpleLongProperty(0); private final LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener(); private final DownloadListener downloadListener = new DownloadListener();
@Getter @Getter
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0); private final LongProperty numUpdates = new SimpleLongProperty(0);
private Socks5ProxyProvider socks5ProxyProvider; private Socks5ProxyProvider socks5ProxyProvider;
@ -101,6 +110,7 @@ public final class XmrConnectionService {
private Boolean isConnected = false; private Boolean isConnected = false;
@Getter @Getter
private MoneroDaemonInfo lastInfo; private MoneroDaemonInfo lastInfo;
private Long lastFallbackInvocation;
private Long lastLogPollErrorTimestamp; private Long lastLogPollErrorTimestamp;
private long lastLogDaemonNotSyncedTimestamp; private long lastLogDaemonNotSyncedTimestamp;
private Long syncStartHeight; private Long syncStartHeight;
@ -109,6 +119,7 @@ public final class XmrConnectionService {
@Getter @Getter
private boolean isShutDownStarted; private boolean isShutDownStarted;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>(); private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
private XmrKeyImagePoller keyImagePoller;
// connection switching // connection switching
private static final int EXCLUDE_CONNECTION_SECONDS = 180; private static final int EXCLUDE_CONNECTION_SECONDS = 180;
@ -117,6 +128,9 @@ public final class XmrConnectionService {
private int numRequestsLastMinute; private int numRequestsLastMinute;
private long lastSwitchTimestamp; private long lastSwitchTimestamp;
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>(); 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 @Inject
public XmrConnectionService(P2PService p2PService, public XmrConnectionService(P2PService p2PService,
@ -144,7 +158,13 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() { p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
ThreadUtils.submitToPool(() -> {
try {
initialize(); initialize();
} catch (Exception e) {
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
}
});
} }
@Override @Override
public void onHiddenServicePublished() {} public void onHiddenServicePublished() {}
@ -250,18 +270,29 @@ public final class XmrConnectionService {
updatePolling(); updatePolling();
} }
public MoneroRpcConnection getBestAvailableConnection() { public MoneroRpcConnection getBestConnection() {
accountService.checkAccountOpen(); return getBestConnection(new ArrayList<MoneroRpcConnection>());
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
addLocalNodeIfIgnored(ignoredConnections);
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
} }
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) { private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
// 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); Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet); addLocalNodeIfIgnored(ignoredConnectionsSet);
return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); 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) { private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
@ -273,7 +304,7 @@ public final class XmrConnectionService {
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled"); log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
return; return;
} }
MoneroRpcConnection bestConnection = getBestAvailableConnection(); MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); if (bestConnection != null) setConnection(bestConnection);
} }
@ -324,7 +355,7 @@ public final class XmrConnectionService {
if (currentConnection != null) excludedConnections.add(currentConnection); if (currentConnection != null) excludedConnections.add(currentConnection);
// get connection to switch to // get connection to switch to
MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
// remove from excluded connections after period // remove from excluded connections after period
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
@ -332,7 +363,7 @@ public final class XmrConnectionService {
}, EXCLUDE_CONNECTION_SECONDS); }, EXCLUDE_CONNECTION_SECONDS);
// return if no connection to switch to // return if no connection to switch to
if (bestConnection == null) { if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
log.warn("No connection to switch to"); log.warn("No connection to switch to");
return false; return false;
} }
@ -388,14 +419,25 @@ public final class XmrConnectionService {
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
} }
// ----------------------------- APP METHODS ------------------------------ public XmrKeyImagePoller getKeyImagePoller() {
synchronized (lock) {
public ReadOnlyIntegerProperty numPeersProperty() { if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
return numPeers; return keyImagePoller;
}
} }
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() { private long getKeyImageRefreshPeriodMs() {
return peers; return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
// ----------------------------- APP METHODS ------------------------------
public ReadOnlyIntegerProperty numConnectionsProperty() {
return numConnections;
}
public ReadOnlyObjectProperty<List<MoneroRpcConnection>> connectionsProperty() {
return connections;
} }
public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() { public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() {
@ -403,7 +445,7 @@ public final class XmrConnectionService {
} }
public boolean hasSufficientPeersForBroadcast() { public boolean hasSufficientPeersForBroadcast() {
return numPeers.get() >= getMinBroadcastConnections(); return numConnections.get() >= getMinBroadcastConnections();
} }
public LongProperty chainHeightProperty() { public LongProperty chainHeightProperty() {
@ -426,6 +468,24 @@ public final class XmrConnectionService {
return numUpdates; 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 -------------------------------- // ------------------------------- HELPERS --------------------------------
private void doneDownload() { private void doneDownload() {
@ -460,6 +520,13 @@ public final class XmrConnectionService {
private void initialize() { 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 // initialize connections
initializeConnections(); initializeConnections();
@ -526,8 +593,13 @@ public final class XmrConnectionService {
// update connection // update connection
if (isConnected) { if (isConnected) {
setConnection(connection.getUri()); 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())) { } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestAvailableConnection(); MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); // switch to best connection if (bestConnection != null) setConnection(bestConnection); // switch to best connection
} }
} }
@ -535,7 +607,7 @@ public final class XmrConnectionService {
} }
// restore connections // restore connections
if ("".equals(config.xmrNode)) { if (!isFixedConnection()) {
// load previous or default connections // load previous or default connections
if (coreContext.isApiUser()) { if (coreContext.isApiUser()) {
@ -547,9 +619,11 @@ public final class XmrConnectionService {
// add default connections // add default connections
for (XmrNode node : xmrNodes.getAllXmrNodes()) { for (XmrNode node : xmrNodes.getAllXmrNodes()) {
if (node.hasClearNetAddress()) { if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); 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 (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
} }
}
if (node.hasOnionAddress()) { if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
@ -560,9 +634,11 @@ public final class XmrConnectionService {
// add default connections // add default connections
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress()) { if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection); addConnection(connection);
} }
}
if (node.hasOnionAddress()) { if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection); addConnection(connection);
@ -571,15 +647,17 @@ public final class XmrConnectionService {
} }
// restore last connection // restore last connection
if (isFixedConnection()) { if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size());
connectionManager.setConnection(getConnections().get(0));
} else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) { if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) {
connectionManager.setConnection(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 // set connection proxies
log.info("TOR proxy URI: " + getProxyUri()); log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) { for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@ -590,12 +668,9 @@ public final class XmrConnectionService {
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
// start local node if applicable
maybeStartLocalNode();
// update connection // update connection
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
MoneroRpcConnection bestConnection = getBestAvailableConnection(); MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); if (bestConnection != null) setConnection(bestConnection);
} }
} else if (!isInitialized) { } else if (!isInitialized) {
@ -605,9 +680,6 @@ public final class XmrConnectionService {
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1); MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
connectionManager.setConnection(connection); connectionManager.setConnection(connection);
// start local node if applicable
maybeStartLocalNode();
} }
// register connection listener // register connection listener
@ -616,30 +688,26 @@ public final class XmrConnectionService {
} }
// notify initial connection // notify initial connection
lastRefreshPeriodMs = getRefreshPeriodMs();
onConnectionChanged(connectionManager.getConnection()); onConnectionChanged(connectionManager.getConnection());
} }
private void maybeStartLocalNode() { public void startLocalNode() throws Exception {
// skip if seed node // cannot start local node as seed node
if (HavenoUtils.isSeedNode()) return; if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node");
}
// start local node if offline and used as last connection // start local node
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
try {
log.info("Starting local node"); log.info("Starting local node");
xmrLocalNode.start(); xmrLocalNode.start();
} catch (Exception e) {
log.error("Unable to start local monero node, error={}\n", e.getMessage(), e);
}
}
} }
private void onConnectionChanged(MoneroRpcConnection currentConnection) { private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return; if (isShutDownStarted || !accountService.isAccountOpen()) return;
if (currentConnection == null) { if (currentConnection == null) {
log.warn("Setting daemon connection to null"); log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
Thread.dumpStack();
} }
synchronized (lock) { synchronized (lock) {
if (currentConnection == null) { if (currentConnection == null) {
@ -661,6 +729,10 @@ public final class XmrConnectionService {
}); });
} }
// update key image poller
keyImagePoller.setDaemon(getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling // update polling
doPollDaemon(); doPollDaemon();
if (currentConnection != getConnection()) return; // polling can change connection if (currentConnection != getConnection()) return; // polling can change connection
@ -709,25 +781,31 @@ public final class XmrConnectionService {
try { try {
// poll daemon // poll daemon
if (daemon == null) switchToBestConnection(); if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
try { try {
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo(); lastInfo = daemon.getInfo();
} catch (Exception e) { } catch (Exception e) {
// skip handling if shutting down // skip handling if shutting down
if (isShutDownStarted) return; if (isShutDownStarted) return;
// fallback to provided or public nodes if custom connection fails on startup // invoke fallback handling on startup error
if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) { boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (xmrNodes.getProvidedXmrNodes().isEmpty()) { if (lastInfo == null && canFallback) {
log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage()); if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); 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 { } else {
log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage()); log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
}
} }
initializeConnections();
return; return;
} }
@ -740,11 +818,13 @@ public final class XmrConnectionService {
// switch to best connection // switch to best connection
switchToBestConnection(); switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = daemon.getInfo(); // caught internally if still fails lastInfo = daemon.getInfo(); // caught internally if still fails
} }
// connected to daemon // connected to daemon
isConnected = true; isConnected = true;
connectionServiceFallbackType.set(null);
// determine if blockchain is syncing locally // 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 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
@ -782,16 +862,15 @@ public final class XmrConnectionService {
downloadListener.progress(percent, blocksLeft, null); downloadListener.progress(percent, blocksLeft, null);
} }
// set peer connections // set available connections
// TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections List<MoneroRpcConnection> availableConnections = new ArrayList<>();
// try { for (MoneroRpcConnection connection : connectionManager.getConnections()) {
// peers.set(getOnlinePeers()); if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) {
// } catch (Exception err) { availableConnections.add(connection);
// // TODO: peers unknown due to restricted RPC call }
// } }
// numPeers.set(peers.get().size()); connections.set(availableConnections);
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); numConnections.set(availableConnections.size());
peers.set(new ArrayList<MoneroPeer>());
// notify update // notify update
numUpdates.set(numUpdates.get() + 1); numUpdates.set(numUpdates.get() + 1);
@ -821,13 +900,15 @@ public final class XmrConnectionService {
} }
} }
private List<MoneroPeer> getOnlinePeers() { private boolean isFixedConnection() {
return daemon.getPeers().stream() return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
.filter(peer -> peer.isOnline())
.collect(Collectors.toList());
} }
private boolean isFixedConnection() { private boolean isCustomConnections() {
return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; 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.user.Preferences;
import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.XmrNodeSettings;
import haveno.core.xmr.nodes.XmrNodes; 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 haveno.core.xmr.wallet.XmrWalletService;
import java.io.File; import java.io.File;
@ -55,6 +57,7 @@ public class XmrLocalNode {
private MoneroConnectionManager connectionManager; private MoneroConnectionManager connectionManager;
private final Config config; private final Config config;
private final Preferences preferences; private final Preferences preferences;
private final XmrNodes xmrNodes;
private final List<XmrLocalNodeListener> listeners = new ArrayList<>(); private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
// required arguments // required arguments
@ -69,9 +72,12 @@ public class XmrLocalNode {
} }
@Inject @Inject
public XmrLocalNode(Config config, Preferences preferences) { public XmrLocalNode(Config config,
Preferences preferences,
XmrNodes xmrNodes) {
this.config = config; this.config = config;
this.preferences = preferences; this.preferences = preferences;
this.xmrNodes = xmrNodes;
this.daemon = new MoneroDaemonRpc(getUri()); this.daemon = new MoneroDaemonRpc(getUri());
// initialize connection manager to listen to local connection // initialize connection manager to listen to local connection
@ -101,7 +107,20 @@ public class XmrLocalNode {
* Returns whether Haveno should ignore a local Monero node even if it is usable. * Returns whether Haveno should ignore a local Monero node even if it is usable.
*/ */
public boolean shouldBeIgnored() { 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) { public void addListener(XmrLocalNodeListener listener) {
@ -120,7 +139,11 @@ public class XmrLocalNode {
} }
public boolean equalsUri(String uri) { public boolean equalsUri(String uri) {
try {
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
} catch (Exception e) {
return false;
}
} }
/** /**
@ -166,11 +189,18 @@ public class XmrLocalNode {
var args = new ArrayList<>(MONEROD_ARGS); var args = new ArrayList<>(MONEROD_ARGS);
var dataDir = settings.getBlockchainPath(); var dataDir = "";
if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) { if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR; dataDir = MONEROD_DATADIR;
} }
if (dataDir != null) args.add("--data-dir=" + dataDir); } else {
dataDir = config.xmrBlockchainPath; // startup config overrides settings
}
if (dataDir != null && !dataDir.isEmpty()) {
args.add("--data-dir=" + dataDir);
}
var bootstrapUrl = settings.getBootstrapUrl(); var bootstrapUrl = settings.getBootstrapUrl();
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) { if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {

View File

@ -78,6 +78,9 @@ public class OfferInfo implements Payload {
@Nullable @Nullable
private final String splitOutputTxHash; private final String splitOutputTxHash;
private final long splitOutputTxFee; private final long splitOutputTxFee;
private final boolean isPrivateOffer;
private final String challenge;
private final String extraInfo;
public OfferInfo(OfferInfoBuilder builder) { public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId(); this.id = builder.getId();
@ -111,6 +114,9 @@ public class OfferInfo implements Payload {
this.arbitratorSigner = builder.getArbitratorSigner(); this.arbitratorSigner = builder.getArbitratorSigner();
this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxHash = builder.getSplitOutputTxHash();
this.splitOutputTxFee = builder.getSplitOutputTxFee(); this.splitOutputTxFee = builder.getSplitOutputTxFee();
this.isPrivateOffer = builder.isPrivateOffer();
this.challenge = builder.getChallenge();
this.extraInfo = builder.getExtraInfo();
} }
public static OfferInfo toOfferInfo(Offer offer) { public static OfferInfo toOfferInfo(Offer offer) {
@ -137,6 +143,7 @@ public class OfferInfo implements Payload {
.withIsActivated(isActivated) .withIsActivated(isActivated)
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash()) .withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee()) .withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
.withChallenge(openOffer.getChallenge())
.build(); .build();
} }
@ -177,7 +184,10 @@ public class OfferInfo implements Payload {
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
.withVersionNumber(offer.getOfferPayload().getVersionNr()) .withVersionNumber(offer.getOfferPayload().getVersionNr())
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) .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) .setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber) .setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion) .setProtocolVersion(protocolVersion)
.setSplitOutputTxFee(splitOutputTxFee); .setSplitOutputTxFee(splitOutputTxFee)
.setIsPrivateOffer(isPrivateOffer);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return builder.build(); return builder.build();
} }
@ -255,6 +268,9 @@ public class OfferInfo implements Payload {
.withArbitratorSigner(proto.getArbitratorSigner()) .withArbitratorSigner(proto.getArbitratorSigner())
.withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxHash(proto.getSplitOutputTxHash())
.withSplitOutputTxFee(proto.getSplitOutputTxFee()) .withSplitOutputTxFee(proto.getSplitOutputTxFee())
.withIsPrivateOffer(proto.getIsPrivateOffer())
.withChallenge(proto.getChallenge())
.withExtraInfo(proto.getExtraInfo())
.build(); .build();
} }
} }

View File

@ -77,7 +77,8 @@ public final class PaymentAccountForm implements PersistablePayload {
AUSTRALIA_PAYID, AUSTRALIA_PAYID,
CASH_APP, CASH_APP,
PAYPAL, PAYPAL,
VENMO; VENMO,
PAYSAFE;
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());

View File

@ -172,14 +172,14 @@ public class TradeInfo implements Payload {
.withAmount(trade.getAmount().longValueExact()) .withAmount(trade.getAmount().longValueExact())
.withMakerFee(trade.getMakerFee().longValueExact()) .withMakerFee(trade.getMakerFee().longValueExact())
.withTakerFee(trade.getTakerFee().longValueExact()) .withTakerFee(trade.getTakerFee().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact()) .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact()) .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact()) .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact()) .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact()) .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact()) .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact()) .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact()) .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
.withTotalTxFee(trade.getTotalTxFee().longValueExact()) .withTotalTxFee(trade.getTotalTxFee().longValueExact())
.withPrice(toPreciseTradePrice.apply(trade)) .withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade)) .withVolume(toRoundedVolume.apply(trade))

View File

@ -63,6 +63,9 @@ public final class OfferInfoBuilder {
private String arbitratorSigner; private String arbitratorSigner;
private String splitOutputTxHash; private String splitOutputTxHash;
private long splitOutputTxFee; private long splitOutputTxFee;
private boolean isPrivateOffer;
private String challenge;
private String extraInfo;
public OfferInfoBuilder withId(String id) { public OfferInfoBuilder withId(String id) {
this.id = id; this.id = id;
@ -234,6 +237,21 @@ public final class OfferInfoBuilder {
return this; 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() { public OfferInfo build() {
return new OfferInfo(this); return new OfferInfo(this);
} }

View File

@ -73,7 +73,7 @@ public class AppStartupState {
isWalletSynced.set(true); isWalletSynced.set(true);
}); });
xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> { xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> {
if (xmrConnectionService.hasSufficientPeersForBroadcast()) if (xmrConnectionService.hasSufficientPeersForBroadcast())
hasSufficientPeersForBroadcast.set(true); hasSufficientPeersForBroadcast.set(true);
}); });

View File

@ -178,6 +178,9 @@ public class DomainInitialisation {
closedTradableManager.onAllServicesInitialized(); closedTradableManager.onAllServicesInitialized();
failedTradesManager.onAllServicesInitialized(); failedTradesManager.onAllServicesInitialized();
filterManager.setFilterWarningHandler(filterWarningHandler);
filterManager.onAllServicesInitialized();
openOfferManager.onAllServicesInitialized(); openOfferManager.onAllServicesInitialized();
balances.onAllServicesInitialized(); balances.onAllServicesInitialized();
@ -199,10 +202,6 @@ public class DomainInitialisation {
priceFeedService.setCurrencyCodeOnInit(); priceFeedService.setCurrencyCodeOnInit();
priceFeedService.startRequestingPrices(); priceFeedService.startRequestingPrices();
filterManager.setFilterWarningHandler(filterWarningHandler);
filterManager.onAllServicesInitialized();
mobileNotificationService.onAllServicesInitialized(); mobileNotificationService.onAllServicesInitialized();
myOfferTakenEvents.onAllServicesInitialized(); myOfferTakenEvents.onAllServicesInitialized();
tradeEvents.onAllServicesInitialized(); tradeEvents.onAllServicesInitialized();

View File

@ -100,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
protected AppModule module; protected AppModule module;
protected Config config; protected Config config;
@Getter @Getter
protected boolean isShutdownInProgress; protected boolean isShutDownStarted;
private boolean isReadOnly; private boolean isReadOnly;
private Thread keepRunningThread; private Thread keepRunningThread;
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS); private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
@ -330,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) { public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
log.info("Starting graceful shut down of {}", getClass().getSimpleName()); log.info("Starting graceful shut down of {}", getClass().getSimpleName());
// ignore if shut down in progress // ignore if shut down started
if (isShutdownInProgress) { if (isShutDownStarted) {
log.info("Ignoring call to gracefulShutDown, already in progress"); log.info("Ignoring call to gracefulShutDown, already started");
return; return;
} }
isShutdownInProgress = true; isShutDownStarted = true;
ResultHandler resultHandler; ResultHandler resultHandler;
if (shutdownCompletedHandler != null) { if (shutdownCompletedHandler != null) {
@ -357,46 +357,47 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// notify trade protocols and wallets to prepare for shut down before shutting down // notify trade protocols and wallets to prepare for shut down before shutting down
Set<Runnable> tasks = new HashSet<Runnable>(); Set<Runnable> tasks = new HashSet<Runnable>();
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
try { try {
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e); 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(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager // shut down open offer manager
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); log.info("Shutting down OpenOfferManager");
injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> {
// shut down offer book service // listen for shut down of wallets setup
injector.getInstance(OfferBookService.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
// shut down p2p service // shut down p2p service
log.info("Shutting down P2P service");
injector.getInstance(P2PService.class).shutDown(() -> { 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 // done shutting down
log.info("Graceful shutdown completed. Exiting now."); log.info("Graceful shutdown completed. Exiting now.");
module.close(injector); module.close(injector);
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
}); });
});
// 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(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown();
}); });
});
} catch (Throwable t) { } catch (Throwable t) {
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t); log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
completeShutdown(resultHandler, EXIT_FAILURE, systemExit); completeShutdown(resultHandler, EXIT_FAILURE, systemExit);

View File

@ -75,6 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
acceptedHandler.run(); acceptedHandler.run();
}); });
havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show));
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
@ -85,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); 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.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));

View File

@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload; import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext; import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrConnectionService;
import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
import haveno.core.api.XmrLocalNode; import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
@ -158,6 +159,9 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler; rejectedTxErrorMessageHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter
@Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler; private Consumer<Boolean> displayTorNetworkSettingsHandler;
@Setter @Setter
@Nullable @Nullable
@ -173,7 +177,7 @@ public class HavenoSetup {
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler; private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
@Setter @Setter
@Nullable @Nullable
private Runnable showPopupIfInvalidBtcConfigHandler; private Runnable showPopupIfInvalidXmrConfigHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler; private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@ -366,7 +370,7 @@ public class HavenoSetup {
// install monerod // install monerod
File monerodFile = new File(XmrLocalNode.MONEROD_PATH); File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME; 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"); log.info("Installing monerod");
monerodFile.getParentFile().mkdirs(); monerodFile.getParentFile().mkdirs();
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile); FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
@ -376,7 +380,7 @@ public class HavenoSetup {
// install monero-wallet-rpc // install monero-wallet-rpc
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH); File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME; 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"); log.info("Installing monero-wallet-rpc");
moneroWalletRpcFile.getParentFile().mkdirs(); moneroWalletRpcFile.getParentFile().mkdirs();
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile); FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
@ -426,6 +430,12 @@ public class HavenoSetup {
getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
getXmrWalletSyncProgress().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"); log.info("Init P2P network");
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork); havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
@ -452,7 +462,7 @@ public class HavenoSetup {
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
walletAppSetup.init(chainFileLockedExceptionHandler, walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler, showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidBtcConfigHandler, showPopupIfInvalidXmrConfigHandler,
() -> {}, () -> {},
() -> {}); () -> {});
} }
@ -725,6 +735,10 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg(); return xmrConnectionService.getConnectionServiceErrorMsg();
} }
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceFallbackType();
}
public StringProperty getTopErrorMsg() { public StringProperty getTopErrorMsg() {
return topErrorMsg; return topErrorMsg;
} }

View File

@ -87,7 +87,7 @@ public class P2PNetworkSetup {
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> { (state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
String result; String result;
int p2pPeers = (int) numP2pPeers; int p2pPeers = (int) numP2pPeers;

View File

@ -117,10 +117,10 @@ public class WalletAppSetup {
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler, void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
@Nullable Runnable showPopupIfInvalidBtcConfigHandler, @Nullable Runnable showPopupIfInvalidXmrConfigHandler,
Runnable downloadCompleteHandler, Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) { 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<>(); ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
xmrInfoBinding = EasyBind.combine( xmrInfoBinding = EasyBind.combine(
@ -199,8 +199,8 @@ public class WalletAppSetup {
walletInitializedHandler.run(); walletInitializedHandler.run();
}, },
exception -> { exception -> {
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
showPopupIfInvalidBtcConfigHandler.run(); showPopupIfInvalidXmrConfigHandler.run();
} else { } else {
walletServiceException.set(exception); walletServiceException.set(exception);
} }

View File

@ -105,21 +105,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
public void gracefulShutDown(ResultHandler resultHandler) { public void gracefulShutDown(ResultHandler resultHandler) {
log.info("Starting graceful shut down of {}", getClass().getSimpleName()); log.info("Starting graceful shut down of {}", getClass().getSimpleName());
// ignore if shut down in progress // ignore if shut down started
if (isShutdownInProgress) { if (isShutDownStarted) {
log.info("Ignoring call to gracefulShutDown, already in progress"); log.info("Ignoring call to gracefulShutDown, already started");
return; return;
} }
isShutdownInProgress = true; isShutDownStarted = true;
try { try {
if (injector != null) { if (injector != null) {
// notify trade protocols and wallets to prepare for shut down // notify trade protocols and wallets to prepare for shut down
Set<Runnable> tasks = new HashSet<Runnable>(); Set<Runnable> tasks = new HashSet<Runnable>();
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
try { try {
ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout
} catch (Exception e) { } catch (Exception e) {
@ -127,25 +127,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
} }
JsonFileManager.shutDownAllInstances(); JsonFileManager.shutDownAllInstances();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager // shut down open offer manager
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); log.info("Shutting down OpenOfferManager");
injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> {
// shut down offer book service // listen for shut down of wallets setup
injector.getInstance(OfferBookService.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
// shut down p2p service // shut down p2p service
log.info("Shutting down P2P service");
injector.getInstance(P2PService.class).shutDown(() -> { 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) -> {
module.close(injector); module.close(injector);
PersistenceManager.flushAllDataToDiskAtShutdown(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
@ -155,6 +151,11 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); 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(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown();

View File

@ -406,6 +406,10 @@ public class FilterManager {
.anyMatch(e -> e.equals(address)); .anyMatch(e -> e.equals(address));
} }
public String getDisableTradeBelowVersion() {
return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
}
public boolean requireUpdateToNewVersionForTrading() { public boolean requireUpdateToNewVersionForTrading() {
if (getFilter() == null) { if (getFilter() == null) {
return false; return false;

View File

@ -200,7 +200,10 @@ public class CurrencyUtil {
result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("ETH", "Ether"));
result.add(new CryptoCurrency("LTC", "Litecoin")); 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-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); result.sort(TradeCurrency::compareTo);
return result; return result;
} }
@ -296,7 +299,7 @@ public class CurrencyUtil {
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) { if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
return isCryptoCurrencyMap.get(currencyCode.toUpperCase()); return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
} }
if (isCryptoCurrencyBase(currencyCode)) { if (isCryptoCurrencyCodeBase(currencyCode)) {
return true; return true;
} }
@ -325,16 +328,18 @@ public class CurrencyUtil {
return isCryptoCurrency; return isCryptoCurrency;
} }
private static boolean isCryptoCurrencyBase(String currencyCode) { private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return false; if (currencyCode == null) return false;
currencyCode = currencyCode.toUpperCase(); currencyCode = currencyCode.toUpperCase();
return currencyCode.equals("USDT"); return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
} }
public static String getCurrencyCodeBase(String currencyCode) { public static String getCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return null; if (currencyCode == null) return null;
currencyCode = currencyCode.toUpperCase(); currencyCode = currencyCode.toUpperCase();
if (currencyCode.contains("USDT")) return "USDT"; if (currencyCode.contains("USDT")) return "USDT";
if (currencyCode.contains("USDC")) return "USDC";
if (currencyCode.contains("DAI")) return "DAI";
return currencyCode; return currencyCode;
} }

View File

@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil; import haveno.core.util.coin.CoinUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
@ -94,6 +92,7 @@ public class CreateOfferService {
Version.VERSION.replace(".", ""); Version.VERSION.replace(".", "");
} }
// TODO: add trigger price?
public Offer createAndGetOffer(String offerId, public Offer createAndGetOffer(String offerId,
OfferDirection direction, OfferDirection direction,
String currencyCode, String currencyCode,
@ -102,10 +101,12 @@ public class CreateOfferService {
Price fixedPrice, Price fixedPrice,
boolean useMarketBasedPrice, boolean useMarketBasedPrice,
double marketPriceMargin, double marketPriceMargin,
double securityDepositAsDouble, double securityDepositPct,
PaymentAccount paymentAccount) { PaymentAccount paymentAccount,
boolean isPrivateOffer,
log.info("create and get offer with offerId={}, " + boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
log.info("Create and get offer with offerId={}, " +
"currencyCode={}, " + "currencyCode={}, " +
"direction={}, " + "direction={}, " +
"fixedPrice={}, " + "fixedPrice={}, " +
@ -113,7 +114,10 @@ public class CreateOfferService {
"marketPriceMargin={}, " + "marketPriceMargin={}, " +
"amount={}, " + "amount={}, " +
"minAmount={}, " + "minAmount={}, " +
"securityDeposit={}", "securityDepositPct={}, " +
"isPrivateOffer={}, " +
"buyerAsTakerWithoutDeposit={}, " +
"extraInfo={}",
offerId, offerId,
currencyCode, currencyCode,
direction, direction,
@ -122,7 +126,19 @@ public class CreateOfferService {
marketPriceMargin, marketPriceMargin,
amount, amount,
minAmount, 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 // verify fixed price xor market price with margin
if (fixedPrice != null) { if (fixedPrice != null) {
@ -130,25 +146,29 @@ public class CreateOfferService {
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price"); if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
} }
long creationTime = new Date().getTime(); // verify price
NodeAddress makerAddress = p2PService.getAddress();
boolean useMarketBasedPriceValue = fixedPrice == null && boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice && useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) && isMarketPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
// verify price
if (fixedPrice == null && !useMarketBasedPriceValue) { if (fixedPrice == null && !useMarketBasedPriceValue) {
throw new IllegalArgumentException("Must provide fixed price"); throw new IllegalArgumentException("Must provide fixed price");
} }
// adjust amount and min amount for fixed-price offer // adjust amount and min amount
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
if (fixedPrice != null) { minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, 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; long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
long amountAsLong = amount != null ? amount.longValueExact() : 0L; long amountAsLong = amount != null ? amount.longValueExact() : 0L;
@ -161,21 +181,16 @@ public class CreateOfferService {
String bankId = PaymentAccountUtil.getBankId(paymentAccount); String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod(); long maxTradePeriod = paymentAccount.getMaxTradePeriod();
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
// reserved for future use cases long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
// Use null values if not set
boolean isPrivateOffer = false;
boolean useAutoClose = false; boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false; boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0; long lowerClosePrice = 0;
long upperClosePrice = 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( offerUtil.validateOfferData(
securityDepositAsDouble, securityDepositPct,
paymentAccount, paymentAccount,
currencyCode); currencyCode);
@ -189,11 +204,11 @@ public class CreateOfferService {
useMarketBasedPriceValue, useMarketBasedPriceValue,
amountAsLong, amountAsLong,
minAmountAsLong, minAmountAsLong,
HavenoUtils.MAKER_FEE_PCT, hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
HavenoUtils.TAKER_FEE_PCT, hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.PENALTY_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT,
securityDepositAsDouble, hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
securityDepositAsDouble, securityDepositPct,
baseCurrencyCode, baseCurrencyCode,
counterCurrencyCode, counterCurrencyCode,
paymentAccount.getPaymentMethod().getId(), paymentAccount.getPaymentMethod().getId(),
@ -211,44 +226,110 @@ public class CreateOfferService {
upperClosePrice, upperClosePrice,
lowerClosePrice, lowerClosePrice,
isPrivateOffer, isPrivateOffer,
hashOfChallenge, challengeHash,
extraDataMap, extraDataMap,
Version.TRADE_PROTOCOL_VERSION, Version.TRADE_PROTOCOL_VERSION,
null, null,
null, null,
null); null,
extraInfo);
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
offer.setChallenge(challenge);
return offer; return offer;
} }
public BigInteger getReservedFundsForOffer(OfferDirection direction, // TODO: add trigger price?
BigInteger amount, public Offer createClonedOffer(Offer sourceOffer,
double buyerSecurityDeposit, String currencyCode,
double sellerSecurityDeposit) { 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, OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
amount, String newOfferId = OfferUtil.getRandomOfferId();
buyerSecurityDeposit, Offer editedOffer = createAndGetOffer(newOfferId,
sellerSecurityDeposit); sourceOfferPayload.getDirection(),
if (!offerUtil.isBuyOffer(direction)) currencyCode,
reservedFundsForOffer = reservedFundsForOffer.add(amount); BigInteger.valueOf(sourceOfferPayload.getAmount()),
BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
fixedPrice,
useMarketBasedPrice,
marketPriceMargin,
sourceOfferPayload.getSellerSecurityDepositPct(),
paymentAccount,
sourceOfferPayload.isPrivateOffer(),
sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
extraInfo);
return reservedFundsForOffer; // generate one-time challenge for private offer
String challenge = null;
String challengeHash = null;
if (sourceOfferPayload.isPrivateOffer()) {
challenge = HavenoUtils.generateChallenge();
challengeHash = HavenoUtils.getChallengeHash(challenge);
} }
public BigInteger getSecurityDeposit(OfferDirection direction, OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
BigInteger amount, long date = new Date().getTime();
double buyerSecurityDeposit, OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
double sellerSecurityDeposit) { date,
return offerUtil.isBuyOffer(direction) ? sourceOfferPayload.getOwnerNodeAddress(),
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) : sourceOfferPayload.getPubKeyRing(),
getSellerSecurityDeposit(amount, sellerSecurityDeposit); sourceOfferPayload.getDirection(),
} editedOfferPayload.getPrice(),
editedOfferPayload.getMarketPriceMarginPct(),
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { editedOfferPayload.isUseMarketBasedPrice(),
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit : sourceOfferPayload.getAmount(),
Restrictions.getSellerSecurityDepositAsPercent(); 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); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice(); 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; package haveno.core.offer;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.common.crypto.KeyRing; import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ErrorMessageHandler;
@ -115,6 +114,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Setter @Setter
transient private boolean isReservedFundsSpent; transient private boolean isReservedFundsSpent;
@JsonExclude
@Getter
@Setter
@Nullable
transient private String challenge;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
@ -275,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
} }
public void setErrorMessage(String errorMessage) { public void setErrorMessage(String errorMessage) {
UserThread.await(() -> errorMessageProperty.set(errorMessage)); errorMessageProperty.set(errorMessage);
} }
@ -337,6 +342,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getSellerSecurityDepositPct(); 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() { public BigInteger getMaxTradeLimit() {
return BigInteger.valueOf(offerPayload.getMaxTradeLimit()); return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
} }
@ -403,7 +420,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
return ""; 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)) if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO); return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO)) else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
@ -414,6 +447,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
return getExtraDataMap().get(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 else
return ""; return "";
} }

View File

@ -36,6 +36,9 @@ package haveno.core.offer;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.file.JsonFileManager; import haveno.common.file.JsonFileManager;
@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager; import haveno.core.filter.FilterManager;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.HashMapChangedListener;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry; import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import haveno.network.utils.Utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroKeyImageSpentStatus;
/** /**
* Handles storage and retrieval of offers. * Handles validation and announcement of offers added or removed.
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
*/ */
@Slf4j
public class OfferBookService { public class OfferBookService {
private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
private final P2PService p2PService; private final P2PService p2PService;
private final PriceFeedService priceFeedService; private final PriceFeedService priceFeedService;
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>(); private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
private final FilterManager filterManager; private final FilterManager filterManager;
private final JsonFileManager jsonFileManager; private final JsonFileManager jsonFileManager;
private final XmrConnectionService xmrConnectionService; private final XmrConnectionService xmrConnectionService;
private final List<Offer> validOffers = new ArrayList<Offer>();
// poll key images of offers private final List<Offer> invalidOffers = new ArrayList<Offer>();
private XmrKeyImagePoller keyImagePoller; private final Map<String, Timer> invalidOfferTimers = new HashMap<>();
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 interface OfferBookChangedListener { public interface OfferBookChangedListener {
void onAdded(Offer offer); void onAdded(Offer offer);
void onRemoved(Offer offer); void onRemoved(Offer offer);
} }
@ -104,51 +113,59 @@ public class OfferBookService {
this.xmrConnectionService = xmrConnectionService; this.xmrConnectionService = xmrConnectionService;
jsonFileManager = new JsonFileManager(storageDir); jsonFileManager = new JsonFileManager(storageDir);
// listen for connection changes to monerod
xmrConnectionService.addConnectionListener((connection) -> {
maybeInitializeKeyImagePoller();
keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
});
// listen for offers // listen for offers
p2PService.addHashSetChangedListener(new HashMapChangedListener() { p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override @Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) { public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
UserThread.execute(() -> { ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> { protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer); synchronized (validOffers) {
synchronized (offerBookChangedListeners) { try {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); 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 @Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) { public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
UserThread.execute(() -> { ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> { protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller(); removeValidOffer(offerPayload.getId());
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer); announceOfferRemoved(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(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());
} }
}); });
@ -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 // API
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public boolean hasOffer(String offerId) {
return hasValidOffer(offerId);
}
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (filterManager.requireUpdateToNewVersionForTrading()) { if (filterManager.requireUpdateToNewVersionForTrading()) {
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
@ -233,16 +264,9 @@ public class OfferBookService {
} }
public List<Offer> getOffers() { public List<Offer> getOffers() {
return p2PService.getDataMap().values().stream() synchronized (validOffers) {
.filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload) return new ArrayList<>(validOffers);
.map(data -> { }
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
return offer;
})
.collect(Collectors.toList());
} }
public List<Offer> getOffersByCurrency(String direction, String currencyCode) { public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
@ -266,7 +290,7 @@ public class OfferBookService {
} }
public void shutDown() { public void shutDown() {
if (keyImagePoller != null) keyImagePoller.clearKeyImages(); xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
} }
@ -274,37 +298,131 @@ public class OfferBookService {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private synchronized void maybeInitializeKeyImagePoller() { private void announceOfferAdded(Offer offer) {
if (keyImagePoller != null) return; xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs()); updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
// handle when key images spent offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
keyImagePoller.addListener(new XmrKeyImageListener() {
@Override
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
UserThread.execute(() -> {
for (String keyImage : spentStatuses.keySet()) {
updateAffectedOffers(keyImage);
} }
});
}
});
// first poll after 20s
// TODO: remove?
new Thread(() -> {
HavenoUtils.waitFor(20000);
keyImagePoller.poll();
}).start();
} }
private long getKeyImageRefreshPeriodMs() { private void announceOfferRemoved(Offer offer) {
return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; 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;
}
}
return false;
}
private void replaceValidOffer(Offer offer) {
synchronized (validOffers) {
removeValidOffer(offer.getId());
validOffers.add(offer);
}
}
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) { private void updateAffectedOffers(String keyImage) {
for (Offer offer : getOffers()) { for (Offer offer : getOffers()) {
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) { synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> { offerBookChangedListeners.forEach(listener -> {
listener.onRemoved(offer); listener.onRemoved(offer);
@ -315,10 +433,9 @@ public class OfferBookService {
} }
} }
private void setReservedFundsSpent(Offer offer) { private void updateReservedFundsSpentStatus(Offer offer) {
if (keyImagePoller == null) return;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) { if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
offer.setReservedFundsSpent(true); offer.setReservedFundsSpent(true);
} }
} }

View File

@ -201,7 +201,7 @@ public class OfferFilterService {
accountAgeWitnessService); accountAgeWitnessService);
long myTradeLimit = accountOptional long myTradeLimit = accountOptional
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getMirroredDirection())) offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
.orElse(0L); .orElse(0L);
long offerMinAmount = offer.getMinAmount().longValueExact(); long offerMinAmount = offer.getMinAmount().longValueExact();
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",

View File

@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; 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 // 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 // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
@ -156,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 // Reserved for possible future use to support private trades where the taker needs to have an accessKey
private final boolean isPrivateOffer; private final boolean isPrivateOffer;
@Nullable @Nullable
private final String hashOfChallenge; private final String challengeHash;
@Nullable
private final String extraInfo;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -195,12 +198,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
long lowerClosePrice, long lowerClosePrice,
long upperClosePrice, long upperClosePrice,
boolean isPrivateOffer, boolean isPrivateOffer,
@Nullable String hashOfChallenge, @Nullable String challengeHash,
@Nullable Map<String, String> extraDataMap, @Nullable Map<String, String> extraDataMap,
int protocolVersion, int protocolVersion,
@Nullable NodeAddress arbitratorSigner, @Nullable NodeAddress arbitratorSigner,
@Nullable byte[] arbitratorSignature, @Nullable byte[] arbitratorSignature,
@Nullable List<String> reserveTxKeyImages) { @Nullable List<String> reserveTxKeyImages,
@Nullable String extraInfo) {
this.id = id; this.id = id;
this.date = date; this.date = date;
this.ownerNodeAddress = ownerNodeAddress; this.ownerNodeAddress = ownerNodeAddress;
@ -238,7 +242,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.lowerClosePrice = lowerClosePrice; this.lowerClosePrice = lowerClosePrice;
this.upperClosePrice = upperClosePrice; this.upperClosePrice = upperClosePrice;
this.isPrivateOffer = isPrivateOffer; this.isPrivateOffer = isPrivateOffer;
this.hashOfChallenge = hashOfChallenge; this.challengeHash = challengeHash;
this.extraInfo = extraInfo;
} }
public byte[] getHash() { public byte[] getHash() {
@ -284,12 +289,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
lowerClosePrice, lowerClosePrice,
upperClosePrice, upperClosePrice,
isPrivateOffer, isPrivateOffer,
hashOfChallenge, challengeHash,
extraDataMap, extraDataMap,
protocolVersion, protocolVersion,
arbitratorSigner, arbitratorSigner,
null, null,
reserveTxKeyImages reserveTxKeyImages,
null
); );
return signee.getHash(); return signee.getHash();
@ -328,12 +334,21 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) { public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct()); 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) { public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct()); 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;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -376,11 +391,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(bankId).ifPresent(builder::setBankId);
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); 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(extraDataMap).ifPresent(builder::putAllExtraData);
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages); Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
} }
@ -392,7 +408,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedCountryCodesList()); null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList()); null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap(); null : proto.getExtraDataMap();
@ -428,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getLowerClosePrice(), proto.getLowerClosePrice(),
proto.getUpperClosePrice(), proto.getUpperClosePrice(),
proto.getIsPrivateOffer(), proto.getIsPrivateOffer(),
hashOfChallenge, ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
extraDataMapMap, extraDataMapMap,
proto.getProtocolVersion(), proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
reserveTxKeyImages); reserveTxKeyImages,
ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
} }
@Override @Override
@ -475,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice + ",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer + ",\r\n isPrivateOffer=" + isPrivateOffer +
",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + ",\r\n challengeHash='" + challengeHash +
",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\r\n extraInfo='" + extraInfo +
"\r\n} "; "\r\n} ";
} }
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions. // 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. // therefore a failure during trade.
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> { public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
@Override @Override
@ -519,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); 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; return object;
} }
} }

View File

@ -37,6 +37,7 @@ import haveno.core.monetary.Volume;
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; 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.AUSTRALIA_PAYID_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CAPABILITIES; 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.CASHAPP_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_CITY;
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
@ -48,6 +49,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAppAccount;
import haveno.core.payment.CashAtAtmAccount;
import haveno.core.payment.F2FAccount; import haveno.core.payment.F2FAccount;
import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount; import haveno.core.payment.PayPalAccount;
@ -58,8 +60,8 @@ import haveno.core.trade.statistics.ReferralIdService;
import haveno.core.user.AutoConfirmSettings; import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinFormatter;
import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.HashMap; import java.util.HashMap;
@ -120,9 +122,10 @@ public class OfferUtil {
public long getMaxTradeLimit(PaymentAccount paymentAccount, public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode, String currencyCode,
OfferDirection direction) { OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
return paymentAccount != null return paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
: 0; : 0;
} }
@ -216,6 +219,10 @@ public class OfferUtil {
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); 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()); extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) { if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
@ -228,16 +235,16 @@ public class OfferUtil {
return extraDataMap.isEmpty() ? null : extraDataMap; return extraDataMap.isEmpty() ? null : extraDataMap;
} }
public void validateOfferData(double buyerSecurityDeposit, public void validateOfferData(double securityDeposit,
PaymentAccount paymentAccount, PaymentAccount paymentAccount,
String currencyCode) { String currencyCode) {
checkNotNull(p2PService.getAddress(), "Address must not be null"); checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
"securityDeposit must not exceed " + "securityDeposit must not exceed " +
getMaxBuyerSecurityDepositAsPercent()); getMaxSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
"securityDeposit must not be less than " + "securityDeposit must not be less than " +
getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit); getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode), checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned")); Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),

View File

@ -48,6 +48,7 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
@EqualsAndHashCode @EqualsAndHashCode
public final class OpenOffer implements Tradable { public final class OpenOffer implements Tradable {
@ -96,6 +97,9 @@ public final class OpenOffer implements Tradable {
@Getter @Getter
private String reserveTxKey; private String reserveTxKey;
@Getter @Getter
@Setter
private String challenge;
@Getter
private final long triggerPrice; private final long triggerPrice;
@Getter @Getter
@Setter @Setter
@ -107,6 +111,12 @@ public final class OpenOffer implements Tradable {
@Getter @Getter
@Setter @Setter
transient int numProcessingAttempts = 0; transient int numProcessingAttempts = 0;
@Getter
@Setter
private boolean deactivatedByTrigger;
@Getter
@Setter
private String groupId;
public OpenOffer(Offer offer) { public OpenOffer(Offer offer) {
this(offer, 0, false); this(offer, 0, false);
@ -120,6 +130,8 @@ public final class OpenOffer implements Tradable {
this.offer = offer; this.offer = offer;
this.triggerPrice = triggerPrice; this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount; this.reserveExactAmount = reserveExactAmount;
this.challenge = offer.getChallenge();
this.groupId = UUID.randomUUID().toString();
state = State.PENDING; state = State.PENDING;
} }
@ -137,6 +149,9 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHash = openOffer.reserveTxHash;
this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey; this.reserveTxKey = openOffer.reserveTxKey;
this.challenge = openOffer.challenge;
this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
this.groupId = openOffer.groupId;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -153,7 +168,10 @@ public final class OpenOffer implements Tradable {
long splitOutputTxFee, long splitOutputTxFee,
@Nullable String reserveTxHash, @Nullable String reserveTxHash,
@Nullable String reserveTxHex, @Nullable String reserveTxHex,
@Nullable String reserveTxKey) { @Nullable String reserveTxKey,
@Nullable String challenge,
boolean deactivatedByTrigger,
@Nullable String groupId) {
this.offer = offer; this.offer = offer;
this.state = state; this.state = state;
this.triggerPrice = triggerPrice; this.triggerPrice = triggerPrice;
@ -164,6 +182,10 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = reserveTxHash; this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; 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;
// reset reserved state to available // reset reserved state to available
if (this.state == State.RESERVED) setState(State.AVAILABLE); if (this.state == State.RESERVED) setState(State.AVAILABLE);
@ -176,7 +198,8 @@ public final class OpenOffer implements Tradable {
.setTriggerPrice(triggerPrice) .setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name())) .setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setSplitOutputTxFee(splitOutputTxFee) .setSplitOutputTxFee(splitOutputTxFee)
.setReserveExactAmount(reserveExactAmount); .setReserveExactAmount(reserveExactAmount)
.setDeactivatedByTrigger(deactivatedByTrigger);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount)); Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes)); Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
@ -184,6 +207,8 @@ public final class OpenOffer implements Tradable {
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); 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(); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
} }
@ -199,7 +224,10 @@ public final class OpenOffer implements Tradable {
proto.getSplitOutputTxFee(), proto.getSplitOutputTxFee(),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey())); ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
proto.getDeactivatedByTrigger(),
ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
return openOffer; return openOffer;
} }
@ -226,6 +254,14 @@ public final class OpenOffer implements Tradable {
public void setState(State state) { public void setState(State state) {
this.state = state; this.state = state;
stateProperty.set(state); stateProperty.set(state);
if (state == State.AVAILABLE) {
deactivatedByTrigger = false;
}
}
public void deactivate(boolean deactivatedByTrigger) {
this.deactivatedByTrigger = deactivatedByTrigger;
setState(State.DEACTIVATED);
} }
public ReadOnlyObjectProperty<State> stateProperty() { public ReadOnlyObjectProperty<State> stateProperty() {
@ -257,6 +293,7 @@ public final class OpenOffer implements Tradable {
",\n reserveExactAmount=" + reserveExactAmount + ",\n reserveExactAmount=" + reserveExactAmount +
",\n scheduledAmount=" + scheduledAmount + ",\n scheduledAmount=" + scheduledAmount +
",\n splitOutputTxFee=" + splitOutputTxFee + ",\n splitOutputTxFee=" + splitOutputTxFee +
",\n groupId=" + groupId +
"\n}"; "\n}";
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -47,11 +47,13 @@ public final class SignedOfferList extends PersistableListAsObservable<SignedOff
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder() return protobuf.PersistableEnvelope.newBuilder()
.setSignedOfferList(protobuf.SignedOfferList.newBuilder() .setSignedOfferList(protobuf.SignedOfferList.newBuilder()
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class))) .addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
.build(); .build();
} }
}
public static SignedOfferList fromProto(protobuf.SignedOfferList proto) { public static SignedOfferList fromProto(protobuf.SignedOfferList proto) {
List<SignedOffer> list = proto.getSignedOfferList().stream() List<SignedOffer> list = proto.getSignedOfferList().stream()

View File

@ -92,12 +92,11 @@ public class TriggerPriceService {
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) .filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> { .forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
.filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); .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(); Price price = openOffer.getOffer().getPrice();
if (price == null || marketPrice == null) { if (price == null || marketPrice == null) {
return false; return false;
@ -125,13 +124,12 @@ public class TriggerPriceService {
} }
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
if (wasTriggered(marketPrice, openOffer)) {
String currencyCode = openOffer.getOffer().getCurrencyCode(); String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT; CryptoMoney.SMALLEST_UNIT_EXPONENT;
long triggerPrice = openOffer.getTriggerPrice();
if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
log.info("Market price exceeded the trigger price of the open offer.\n" + log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}", "Market price: {};\nTrigger price: {}",
@ -139,14 +137,26 @@ public class TriggerPriceService {
currencyCode, currencyCode,
openOffer.getOffer().getDirection(), openOffer.getOffer().getDirection(),
marketPrice.getPrice(), 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 -> { }, errorMessage -> {
}); });
} else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
// TODO: check if open offer's reserve tx is failed or double spend seen
} }
} }

View File

@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
null, // reserve tx not sent from taker to maker null, // reserve tx not sent from taker to maker
null, null,
null, null,
payoutAddress); payoutAddress,
null); // challenge is required when offer taken
// save trade request to later send to arbitrator // save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest); model.setTradeRequest(tradeRequest);

View File

@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.offer.messages.SignOfferResponse; import haveno.core.offer.messages.SignOfferResponse;
import haveno.core.offer.placeoffer.tasks.AddToOfferBook; import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook;
import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse; import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse;
import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest; import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest;
@ -31,6 +31,8 @@ import haveno.core.offer.placeoffer.tasks.ValidateOffer;
import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.handlers.TransactionResultHandler;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import org.bitcoinj.core.Transaction;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -39,8 +41,8 @@ public class PlaceOfferProtocol {
private final PlaceOfferModel model; private final PlaceOfferModel model;
private Timer timeoutTimer; private Timer timeoutTimer;
private final TransactionResultHandler resultHandler; private TransactionResultHandler resultHandler;
private final ErrorMessageHandler errorMessageHandler; private ErrorMessageHandler errorMessageHandler;
private TaskRunner<PlaceOfferModel> taskRunner; private TaskRunner<PlaceOfferModel> taskRunner;
@ -89,7 +91,6 @@ public class PlaceOfferProtocol {
handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle
} }
// TODO (woodser): switch to fluent
public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) { public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) {
log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId()); log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId());
model.setSignOfferResponse(response); model.setSignOfferResponse(response);
@ -119,7 +120,7 @@ public class PlaceOfferProtocol {
() -> { () -> {
log.debug("sequence at handleSignOfferResponse completed"); log.debug("sequence at handleSignOfferResponse completed");
stopTimeoutTimer(); stopTimeoutTimer();
resultHandler.handleResult(model.getTransaction()); // TODO (woodser): XMR transaction instead handleResult(model.getTransaction()); // TODO: use XMR transaction instead
}, },
(errorMessage) -> { (errorMessage) -> {
if (model.isOfferAddedToOfferBook()) { if (model.isOfferAddedToOfferBook()) {
@ -135,27 +136,33 @@ public class PlaceOfferProtocol {
); );
taskRunner.addTasks( taskRunner.addTasks(
MakerProcessSignOfferResponse.class, MakerProcessSignOfferResponse.class,
AddToOfferBook.class MaybeAddToOfferBook.class
); );
taskRunner.run(); taskRunner.run();
} }
public void startTimeoutTimer() { public synchronized void startTimeoutTimer() {
if (resultHandler == null) return;
stopTimeoutTimer(); stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> { timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing")); handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
} }
private void stopTimeoutTimer() { private synchronized void stopTimeoutTimer() {
if (timeoutTimer != null) { if (timeoutTimer != null) {
timeoutTimer.stop(); timeoutTimer.stop();
timeoutTimer = null; timeoutTimer = null;
} }
} }
private void handleError(String errorMessage) { private synchronized void handleResult(Transaction transaction) {
resultHandler.handleResult(transaction);
resetHandlers();
}
private synchronized void handleError(String errorMessage) {
if (timeoutTimer != null) { if (timeoutTimer != null) {
taskRunner.cancel(); taskRunner.cancel();
if (!model.getOpenOffer().isCanceled()) { if (!model.getOpenOffer().isCanceled()) {
@ -164,5 +171,11 @@ public class PlaceOfferProtocol {
stopTimeoutTimer(); stopTimeoutTimer();
errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage);
} }
resetHandlers();
}
private synchronized void resetHandlers() {
resultHandler = null;
errorMessageHandler = null;
} }
} }

View File

@ -19,7 +19,9 @@ package haveno.core.offer.placeoffer.tasks;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import haveno.common.taskrunner.Task; import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
@ -33,6 +35,7 @@ import haveno.core.xmr.model.XmrAddressEntry;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@Slf4j @Slf4j
@ -62,7 +65,6 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().getXmrConnectionService().verifyConnection(); model.getXmrWalletService().getXmrConnectionService().verifyConnection();
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = null;
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// reset protocol timeout // reset protocol timeout
@ -78,7 +80,14 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// copy address entries to clones
for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
if (offerClone.getId().equals(offer.getId())) continue; // skip self
model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId());
}
// attempt creating reserve tx // attempt creating reserve tx
MoneroTxWallet reserveTx = null;
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
@ -86,6 +95,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try { try {
//if (true) throw new RuntimeException("Pretend error"); //if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (IllegalStateException e) {
log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage());
throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
model.getXmrWalletService().handleWalletError(e, sourceConnection); model.getXmrWalletService().handleWalletError(e, sourceConnection);
@ -116,11 +128,43 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update offer state // update offer state including clones
if (openOffer.getGroupId() == null) {
openOffer.setReserveTxHash(reserveTx.getHash()); openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex()); openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey()); openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
} else {
for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
offerClone.setReserveTxHash(reserveTx.getHash());
offerClone.setReserveTxHex(reserveTx.getFullHex());
offerClone.setReserveTxKey(reserveTx.getKey());
offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
}
}
// reset offer funding address entries if unused
if (fundingEntry != null) {
// get reserve tx inputs
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
// collect subaddress indices of inputs
Set<Integer> inputSubaddressIndices = new HashSet<>();
for (MoneroOutputWallet input : inputs) {
if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex());
}
// swap funding address entries to available if unused
for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
}
}
}
}
} }
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {

View File

@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
offer.getOfferPayload().getReserveTxKeyImages(), offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress); returnAddress);
// send request to least used arbitrators until success // send request to random arbitrators until success
sendSignOfferRequests(request, () -> { sendSignOfferRequests(request, () -> {
complete(); complete();
}, (errorMessage) -> { }, (errorMessage) -> {

View File

@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks;
import haveno.common.taskrunner.Task; import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferModel;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
public class AddToOfferBook extends Task<PlaceOfferModel> { public class MaybeAddToOfferBook extends Task<PlaceOfferModel> {
public AddToOfferBook(TaskRunner<PlaceOfferModel> taskHandler, PlaceOfferModel model) { public MaybeAddToOfferBook(TaskRunner<PlaceOfferModel> taskHandler, PlaceOfferModel model) {
super(taskHandler, model); super(taskHandler, model);
} }
@ -35,17 +36,32 @@ public class AddToOfferBook extends Task<PlaceOfferModel> {
try { try {
runInterceptHook(); runInterceptHook();
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId());
// deactivate if conflicting offer exists
if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) {
model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED);
model.setOfferAddedToOfferBook(false);
complete();
return;
}
// add to offer book and activate if pending or available
if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) {
model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
() -> { () -> {
model.getOpenOffer().setState(OpenOffer.State.AVAILABLE);
model.setOfferAddedToOfferBook(true); model.setOfferAddedToOfferBook(true);
complete(); complete();
}, },
errorMessage -> { errorMessage -> {
model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
"Please check your network connection and try again."); "Please check your network connection and try again.");
failed(errorMessage); failed(errorMessage);
}); });
} else {
complete();
return;
}
} catch (Throwable t) { } catch (Throwable t) {
model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" + model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" +
"Error message:\n" "Error message:\n"

View File

@ -21,6 +21,7 @@ import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.messages.TradeMessage;
@ -63,8 +64,21 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
offer.isPrivateOffer();
if (offer.isPrivateOffer()) {
boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY;
if (isBuyerMaker) {
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct());
} else {
if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
}
} else {
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
}
// We remove those checks to be more flexible with future changes. // We remove those checks to be more flexible with future changes.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
@ -82,9 +96,9 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection()); long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
checkArgument(offer.getAmount().longValueExact() <= maxAmount, checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR"); "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null"); checkNotNull(offer.getPrice(), "Price is null");

View File

@ -148,7 +148,8 @@ public class TakeOfferModel implements Model {
private long getMaxTradeLimit() { private long getMaxTradeLimit() {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getCurrencyCode(),
offer.getMirroredDirection()); offer.getMirroredDirection(),
offer.hasBuyerAsTakerWithoutDeposit());
} }
@NotNull @NotNull

View File

@ -31,7 +31,34 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class AliPayAccount extends PaymentAccount { public final class AliPayAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
new TraditionalCurrency("AED"),
new TraditionalCurrency("AUD"),
new TraditionalCurrency("CAD"),
new TraditionalCurrency("CHF"),
new TraditionalCurrency("CNY"),
new TraditionalCurrency("CZK"),
new TraditionalCurrency("DKK"),
new TraditionalCurrency("EUR"),
new TraditionalCurrency("GBP"),
new TraditionalCurrency("HKD"),
new TraditionalCurrency("IDR"),
new TraditionalCurrency("ILS"),
new TraditionalCurrency("JPY"),
new TraditionalCurrency("KRW"),
new TraditionalCurrency("LKR"),
new TraditionalCurrency("MUR"),
new TraditionalCurrency("MYR"),
new TraditionalCurrency("NOK"),
new TraditionalCurrency("NZD"),
new TraditionalCurrency("PHP"),
new TraditionalCurrency("RUB"),
new TraditionalCurrency("SEK"),
new TraditionalCurrency("SGD"),
new TraditionalCurrency("THB"),
new TraditionalCurrency("USD"),
new TraditionalCurrency("ZAR")
);
public AliPayAccount() { public AliPayAccount() {
super(PaymentMethod.ALI_PAY); super(PaymentMethod.ALI_PAY);

View File

@ -93,7 +93,7 @@ public final class F2FAccount extends CountryBasedPaymentAccount {
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city")); if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city"));
if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact")); if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact"));
if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt")); if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount"));
return field; return field;
} }
} }

View File

@ -36,6 +36,7 @@ package haveno.core.payment;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import haveno.common.proto.ProtoUtil; import haveno.common.proto.ProtoUtil;
import haveno.common.proto.persistable.PersistablePayload; import haveno.common.proto.persistable.PersistablePayload;
import haveno.common.util.Utilities; import haveno.common.util.Utilities;
@ -341,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload {
// ---------------------------- SERIALIZATION ----------------------------- // ---------------------------- SERIALIZATION -----------------------------
public String toJson() { public String toJson() {
Map<String, Object> jsonMap = new HashMap<String, Object>(); Gson gson = gsonBuilder.create();
if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class)); Map<String, Object> jsonMap = new HashMap<>();
if (paymentAccountPayload != null) {
String payloadJson = paymentAccountPayload.toJson();
Map<String, Object> payloadMap = gson.fromJson(payloadJson, new TypeToken<Map<String, Object>>() {}.getType());
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof List) {
List<?> list = (List<?>) value;
String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(","));
entry.setValue(joinedString);
}
}
jsonMap.putAll(payloadMap);
}
jsonMap.put("accountName", getAccountName()); jsonMap.put("accountName", getAccountName());
jsonMap.put("accountId", getId()); jsonMap.put("accountId", getId());
if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex());
return gsonBuilder.create().toJson(jsonMap); return gson.toJson(jsonMap);
} }
/** /**
@ -378,6 +396,7 @@ public abstract class PaymentAccount implements PersistablePayload {
@NonNull @NonNull
public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds(); public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds();
@SuppressWarnings("unchecked")
public PaymentAccountForm toForm() { public PaymentAccountForm toForm() {
// convert to json map // convert to json map

View File

@ -136,6 +136,8 @@ public class PaymentAccountFactory {
return new CashAppAccount(); return new CashAppAccount();
case PaymentMethod.VENMO_ID: case PaymentMethod.VENMO_ID:
return new VenmoAccount(); return new VenmoAccount();
case PaymentMethod.PAYSAFE_ID:
return new PaysafeAccount();
// Cannot be deleted as it would break old trade history entries // Cannot be deleted as it would break old trade history entries
case PaymentMethod.OK_PAY_ID: case PaymentMethod.OK_PAY_ID:

View File

@ -36,11 +36,13 @@ public class PaymentAccountList extends PersistableList<PaymentAccount> {
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder() return protobuf.PersistableEnvelope.newBuilder()
.setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder()
.addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList())))
.build(); .build();
} }
}
public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) { public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) {
return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream() return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream()

View File

@ -24,7 +24,6 @@ import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import haveno.core.locale.Country; import haveno.core.locale.Country;
import haveno.core.locale.CountryUtil; import haveno.core.locale.CountryUtil;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency; import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload;
@ -42,7 +41,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import static com.google.common.base.Preconditions.checkNotNull;
import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy; import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy;
import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString; import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString;
import static haveno.common.util.ReflectionUtils.handleSetFieldValueError; import static haveno.common.util.ReflectionUtils.handleSetFieldValueError;
@ -50,7 +48,6 @@ import static haveno.common.util.ReflectionUtils.isSetterOnClass;
import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy; import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy;
import static haveno.common.util.Utilities.decodeFromHex; import static haveno.common.util.Utilities.decodeFromHex;
import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.locale.CountryUtil.findCountryByCode;
import static haveno.core.locale.CurrencyUtil.getCurrencyByCountryCode;
import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList;
import static haveno.core.locale.CurrencyUtil.getTradeCurrency; import static haveno.core.locale.CurrencyUtil.getTradeCurrency;
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
@ -435,8 +432,10 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
if (account.isCountryBasedPaymentAccount()) { if (account.isCountryBasedPaymentAccount()) {
((CountryBasedPaymentAccount) account).setCountry(country.get()); ((CountryBasedPaymentAccount) account).setCountry(country.get());
TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
account.setSingleTradeCurrency(fiatCurrency); // TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere?
// TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
// account.setSingleTradeCurrency(fiatCurrency);
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
((MoneyGramAccount) account).setCountry(country.get()); ((MoneyGramAccount) account).setCountry(country.get());
} else { } else {

View File

@ -124,7 +124,7 @@ public class PaymentAccountUtil {
AccountAgeWitnessService accountAgeWitnessService) { AccountAgeWitnessService accountAgeWitnessService) {
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact(); offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
return !hasChargebackRisk || hasValidAccountAgeWitness; return !hasChargebackRisk || hasValidAccountAgeWitness;
} }

View File

@ -0,0 +1,112 @@
/*
* 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.core.payment;
import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.payment.payload.PaysafeAccountPayload;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
public final class PaysafeAccount extends PaymentAccount {
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.EMAIL,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.SALT
);
// https://developer.paysafe.com/en/support/reference-information/codes/
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
new TraditionalCurrency("AED"),
new TraditionalCurrency("ARS"),
new TraditionalCurrency("AUD"),
new TraditionalCurrency("BGN"),
new TraditionalCurrency("BRL"),
new TraditionalCurrency("CAD"),
new TraditionalCurrency("CHF"),
new TraditionalCurrency("CZK"),
new TraditionalCurrency("DKK"),
new TraditionalCurrency("EGP"),
new TraditionalCurrency("EUR"),
new TraditionalCurrency("GBP"),
new TraditionalCurrency("GEL"),
new TraditionalCurrency("HUF"),
new TraditionalCurrency("ILS"),
new TraditionalCurrency("INR"),
new TraditionalCurrency("JPY"),
new TraditionalCurrency("ISK"),
new TraditionalCurrency("KWD"),
new TraditionalCurrency("KRW"),
new TraditionalCurrency("MXN"),
new TraditionalCurrency("NOK"),
new TraditionalCurrency("NZD"),
new TraditionalCurrency("PEN"),
new TraditionalCurrency("PHP"),
new TraditionalCurrency("PLN"),
new TraditionalCurrency("RON"),
new TraditionalCurrency("RSD"),
new TraditionalCurrency("RUB"),
new TraditionalCurrency("SAR"),
new TraditionalCurrency("SEK"),
new TraditionalCurrency("TRY"),
new TraditionalCurrency("USD"),
new TraditionalCurrency("UYU")
);
public PaysafeAccount() {
super(PaymentMethod.PAYSAFE);
}
@Override
protected PaymentAccountPayload createPayload() {
return new PaysafeAccountPayload(paymentMethod.getId(), id);
}
@Override
public @NotNull List<TradeCurrency> getSupportedCurrencies() {
return SUPPORTED_CURRENCIES;
}
@Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
return INPUT_FIELD_IDS;
}
public void setEmail(String accountId) {
((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId);
}
public String getEmail() {
return ((PaysafeAccountPayload) paymentAccountPayload).getEmail();
}
@Override
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
var field = super.getEmptyFormField(fieldId);
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue("");
return field;
}
}

View File

@ -30,7 +30,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Singleton @Singleton
public class TradeLimits { public class TradeLimits {
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that. private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that.
private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer
@Nullable @Nullable
@Getter @Getter
private static TradeLimits INSTANCE; private static TradeLimits INSTANCE;
@ -57,6 +59,15 @@ public class TradeLimits {
return MAX_TRADE_LIMIT; return MAX_TRADE_LIMIT;
} }
/**
* The maximum trade limit without a buyer deposit.
*
* @return the maximum trade limit for a buyer without a deposit
*/
public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() {
return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT;
}
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account // We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
// age witness is not considered anymore (> 2 months). // age witness is not considered anymore (> 2 months).

View File

@ -31,11 +31,15 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class WeChatPayAccount extends PaymentAccount { public final class WeChatPayAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
new TraditionalCurrency("CNY"),
new TraditionalCurrency("USD"),
new TraditionalCurrency("EUR"),
new TraditionalCurrency("GBP")
);
public WeChatPayAccount() { public WeChatPayAccount() {
super(PaymentMethod.WECHAT_PAY); super(PaymentMethod.WECHAT_PAY);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
} }
@Override @Override

View File

@ -51,6 +51,7 @@ import haveno.core.payment.CashAppAccount;
import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.CashAtAtmAccount;
import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount; import haveno.core.payment.PayPalAccount;
import haveno.core.payment.PaysafeAccount;
import haveno.core.payment.CashDepositAccount; import haveno.core.payment.CashDepositAccount;
import haveno.core.payment.CelPayAccount; import haveno.core.payment.CelPayAccount;
import haveno.core.payment.ZelleAccount; import haveno.core.payment.ZelleAccount;
@ -124,13 +125,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) : Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
TimeUnit.DAYS.toMillis(1); TimeUnit.DAYS.toMillis(1);
// Default trade limits. // These values are not used except to derive the associated risk factor.
// We initialize very early before reading persisted data. We will apply later the limit from private static final BigInteger DEFAULT_TRADE_LIMIT_CRYPTO = HavenoUtils.xmrToAtomicUnits(200);
// the DAO param (Param.MAX_TRADE_LIMIT) but that can be only done after the dao is initialized.
// The default values will be used for deriving the
// risk factor so the relation between the risk categories stays the same as with the default values.
// We must not change those values as it could lead to invalid offers if amount becomes lower then new trade limit.
// Increasing might be ok, but needs more thought as well...
private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100); private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100);
private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50); private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50);
private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25); private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25);
@ -198,6 +194,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
public static final String CASH_APP_ID = "CASH_APP"; public static final String CASH_APP_ID = "CASH_APP";
public static final String VENMO_ID = "VENMO"; public static final String VENMO_ID = "VENMO";
public static final String PAYPAL_ID = "PAYPAL"; public static final String PAYPAL_ID = "PAYPAL";
public static final String PAYSAFE_ID = "PAYSAFE";
public static PaymentMethod UPHOLD; public static PaymentMethod UPHOLD;
public static PaymentMethod MONEY_BEAM; public static PaymentMethod MONEY_BEAM;
@ -257,6 +254,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
public static PaymentMethod PAYPAL; public static PaymentMethod PAYPAL;
public static PaymentMethod CASH_APP; public static PaymentMethod CASH_APP;
public static PaymentMethod VENMO; public static PaymentMethod VENMO;
public static PaymentMethod PAYSAFE;
// Cannot be deleted as it would break old trade history entries // Cannot be deleted as it would break old trade history entries
@Deprecated @Deprecated
@ -288,7 +286,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
// Global // Global
CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)), CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)),
PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)), PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)),
CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)), CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)),
MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)), MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)),
WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)), WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)),
@ -327,6 +325,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)), DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)),
PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)), PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)),
CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)), CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)),
PAYSAFE = new PaymentMethod(PaymentMethod.PAYSAFE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaysafeAccount.SUPPORTED_CURRENCIES)),
// Japan // Japan
JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)), JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)),
@ -342,10 +341,10 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)), PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)),
// Cryptos // Cryptos
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()), BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList()),
// Cryptos with 1 hour trade period // Cryptos with 1 hour trade period
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()) BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList())
); );
// TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private // TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private
@ -369,7 +368,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
AUSTRALIA_PAYID_ID, AUSTRALIA_PAYID_ID,
CASH_APP_ID, CASH_APP_ID,
PAYPAL_ID, PAYPAL_ID,
VENMO_ID); VENMO_ID,
PAYSAFE_ID);
return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList());
} }
@ -497,17 +497,21 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
} }
// We use the class field maxTradeLimit only for mapping the risk factor. // We use the class field maxTradeLimit only for mapping the risk factor.
// The actual trade limit is calculated by dividing TradeLimits.MAX_TRADE_LIMIT by the
// risk factor, and then further decreasing by chargeback risk, account signing, and age.
long riskFactor; long riskFactor;
if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact()) if (maxTradeLimit == DEFAULT_TRADE_LIMIT_CRYPTO.longValueExact())
riskFactor = 1; riskFactor = 1;
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact()) else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact())
riskFactor = 2;
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
riskFactor = 4; riskFactor = 4;
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact())
riskFactor = 11;
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
riskFactor = 22;
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact()) else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact())
riskFactor = 8; riskFactor = 44;
else { else {
riskFactor = 8; riskFactor = 44;
log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " + log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " +
"maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this); "maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this);
} }
@ -589,7 +593,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
id.equals(PaymentMethod.UPHOLD_ID) || id.equals(PaymentMethod.UPHOLD_ID) ||
id.equals(PaymentMethod.CASH_APP_ID) || id.equals(PaymentMethod.CASH_APP_ID) ||
id.equals(PaymentMethod.PAYPAL_ID) || id.equals(PaymentMethod.PAYPAL_ID) ||
id.equals(PaymentMethod.VENMO_ID); id.equals(PaymentMethod.VENMO_ID) ||
id.equals(PaymentMethod.PAYSAFE_ID);
} }
public static boolean isRoundedForAtmCash(String id) { public static boolean isRoundedForAtmCash(String id) {
@ -598,7 +603,6 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
} }
public static boolean isFixedPriceOnly(String id) { public static boolean isFixedPriceOnly(String id) {
return id.equals(PaymentMethod.CASH_AT_ATM_ID) || return id.equals(PaymentMethod.HAL_CASH_ID);
id.equals(PaymentMethod.HAL_CASH_ID);
} }
} }

View File

@ -0,0 +1,96 @@
/*
* 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.core.payment.payload;
import com.google.protobuf.Message;
import haveno.core.locale.Res;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@EqualsAndHashCode(callSuper = true)
@ToString
@Setter
@Getter
@Slf4j
public final class PaysafeAccountPayload extends PaymentAccountPayload {
private String email = "";
public PaysafeAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PaysafeAccountPayload(String paymentMethod,
String id,
String email,
long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod,
id,
maxTradePeriod,
excludeFromJsonDataMap);
this.email = email;
}
@Override
public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder()
.setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email))
.build();
}
public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
return new PaysafeAccountPayload(proto.getPaymentMethodId(),
proto.getId(),
proto.getPaysafeAccountPayload().getEmail(),
proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public String getPaymentDetails() {
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email;
}
@Override
public String getPaymentDetailsForTradePopup() {
return getPaymentDetails();
}
@Override
public byte[] getAgeWitnessInputData() {
return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooLowPercentageValue(String input) { private ValidationResult validateIfNotTooLowPercentageValue(String input) {
try { try {
double percentage = ParsingUtils.parsePercentStringToDouble(input); double percentage = ParsingUtils.parsePercentStringToDouble(input);
double minPercentage = Restrictions.getMinBuyerSecurityDepositAsPercent(); double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
if (percentage < minPercentage) if (percentage < minPercentage)
return new ValidationResult(false, return new ValidationResult(false,
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage))); Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooHighPercentageValue(String input) { private ValidationResult validateIfNotTooHighPercentageValue(String input) {
try { try {
double percentage = ParsingUtils.parsePercentStringToDouble(input); double percentage = ParsingUtils.parsePercentStringToDouble(input);
double maxPercentage = Restrictions.getMaxBuyerSecurityDepositAsPercent(); double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
if (percentage > maxPercentage) if (percentage > maxPercentage)
return new ValidationResult(false, return new ValidationResult(false,
Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage)));

View File

@ -54,6 +54,7 @@ import haveno.core.payment.payload.NequiAccountPayload;
import haveno.core.payment.payload.OKPayAccountPayload; import haveno.core.payment.payload.OKPayAccountPayload;
import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaxumAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaysafeAccountPayload;
import haveno.core.payment.payload.PayPalAccountPayload; import haveno.core.payment.payload.PayPalAccountPayload;
import haveno.core.payment.payload.PayseraAccountPayload; import haveno.core.payment.payload.PayseraAccountPayload;
import haveno.core.payment.payload.PaytmAccountPayload; import haveno.core.payment.payload.PaytmAccountPayload;
@ -239,6 +240,8 @@ public class CoreProtoResolver implements ProtoResolver {
return VenmoAccountPayload.fromProto(proto); return VenmoAccountPayload.fromProto(proto);
case PAYPAL_ACCOUNT_PAYLOAD: case PAYPAL_ACCOUNT_PAYLOAD:
return PayPalAccountPayload.fromProto(proto); return PayPalAccountPayload.fromProto(proto);
case PAYSAFE_ACCOUNT_PAYLOAD:
return PaysafeAccountPayload.fromProto(proto);
default: default:
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase); throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase);

View File

@ -52,7 +52,8 @@ public class ProvidersRepository {
private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/"; private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/";
private static final List<String> DEFAULT_NODES = Arrays.asList( private static final List<String> DEFAULT_NODES = Arrays.asList(
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/" // Cake "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake
"http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck
); );
private final Config config; private final Config config;

View File

@ -232,11 +232,13 @@ public abstract class SupportManager {
getAllChatMessages(ackMessage.getSourceId()).stream() getAllChatMessages(ackMessage.getSourceId()).stream()
.filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid()))
.forEach(msg -> { .forEach(msg -> {
UserThread.execute(() -> {
if (ackMessage.isSuccess()) if (ackMessage.isSuccess())
msg.setAcknowledged(true); msg.setAcknowledged(true);
else else
msg.setAckError(ackMessage.getErrorMessage()); msg.setAckError(ackMessage.getErrorMessage());
}); });
});
requestPersistence(); requestPersistence();
} }
} }

View File

@ -467,7 +467,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
} }
public boolean isOpen() { public boolean isOpen() {
return this.disputeState == State.OPEN || this.disputeState == State.REOPENED; return isNew() || this.disputeState == State.OPEN || this.disputeState == State.REOPENED;
} }
public boolean isClosed() { public boolean isClosed() {

View File

@ -74,7 +74,9 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
@Override @Override
public void readPersisted(Runnable completeHandler) { public void readPersisted(Runnable completeHandler) {
persistenceManager.readPersisted(getFileName(), persisted -> { persistenceManager.readPersisted(getFileName(), persisted -> {
synchronized (persisted.getList()) {
disputeList.setAll(persisted.getList()); disputeList.setAll(persisted.getList());
}
completeHandler.run(); completeHandler.run();
}, },
completeHandler); completeHandler);
@ -145,10 +147,13 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
private void onDisputesChangeListener(List<? extends Dispute> addedList, private void onDisputesChangeListener(List<? extends Dispute> addedList,
@Nullable List<? extends Dispute> removedList) { @Nullable List<? extends Dispute> removedList) {
if (removedList != null) { if (removedList != null) {
synchronized (removedList) {
removedList.forEach(dispute -> { removedList.forEach(dispute -> {
disputedTradeIds.remove(dispute.getTradeId()); disputedTradeIds.remove(dispute.getTradeId());
}); });
} }
}
synchronized (addedList) {
addedList.forEach(dispute -> { addedList.forEach(dispute -> {
// for each dispute added, keep track of its "BadgeCountProperty" // for each dispute added, keep track of its "BadgeCountProperty"
EasyBind.subscribe(dispute.getBadgeCountProperty(), EasyBind.subscribe(dispute.getBadgeCountProperty(),
@ -166,6 +171,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
disputedTradeIds.add(dispute.getTradeId()); disputedTradeIds.add(dispute.getTradeId());
}); });
} }
}
public void requestPersistence() { public void requestPersistence() {
persistenceManager.requestPersistence(); persistenceManager.requestPersistence();

View File

@ -288,6 +288,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
cleanupDisputes(); cleanupDisputes();
List<Dispute> disputes = getDisputeList().getList(); List<Dispute> disputes = getDisputeList().getList();
synchronized (disputes) {
disputes.forEach(dispute -> { disputes.forEach(dispute -> {
try { try {
DisputeValidation.validateNodeAddresses(dispute, config); DisputeValidation.validateNodeAddresses(dispute, config);
@ -296,6 +297,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
validationExceptions.add(e); validationExceptions.add(e);
} }
}); });
}
maybeClearSensitiveData(); maybeClearSensitiveData();
} }
@ -318,12 +320,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
public void maybeClearSensitiveData() { public void maybeClearSensitiveData() {
log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName()); log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName());
Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing(); Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing();
synchronized (getDisputeList().getList()) {
getDisputeList().getList().stream() getDisputeList().getList().stream()
.filter(e -> e.isClosed()) .filter(e -> e.isClosed())
.filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate))
.forEach(Dispute::maybeClearSensitiveData); .forEach(Dispute::maybeClearSensitiveData);
requestPersistence(); requestPersistence();
} }
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Dispute handling // Dispute handling
@ -359,6 +363,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return; return;
} }
// skip if payout is confirmed
if (trade.isPayoutConfirmed()) {
String errorMsg = "Cannot open dispute because payout is already confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId();
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
return;
}
synchronized (disputeList.getObservableList()) { synchronized (disputeList.getObservableList()) {
if (disputeList.contains(dispute)) { if (disputeList.contains(dispute)) {
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
@ -368,8 +379,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed(); boolean reOpen = storedDisputeOptional.isPresent();
if (!storedDisputeOptional.isPresent() || reOpen) {
// add or re-open dispute // add or re-open dispute
if (reOpen) { if (reOpen) {
@ -393,13 +403,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
chatMessage.setSystemMessage(true); chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage); dispute.addAndPersistChatMessage(chatMessage);
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// create dispute opened message // create dispute opened message
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
@ -472,12 +475,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
errorMessage, new DisputeMessageDeliveryFailedException()); errorMessage, new DisputeMessageDeliveryFailedException());
} }
}); });
} else {
String msg = "We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId();
log.warn(msg);
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
}
} }
requestPersistence(); requestPersistence();
@ -537,15 +534,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
throw e; throw e;
} }
// try to validate payment account // try to validate payment accounts
try { try {
DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing
} catch (Exception e) { } catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e)); log.error(ExceptionUtils.getStackTrace(e));
trade.prependErrorMessage(e.getMessage()); trade.prependErrorMessage(e.getMessage());
throw e; throw e;
} }
// set arbitrator's payment account payloads
if (trade.isArbitrator()) {
if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload());
if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload());
}
// get sender // get sender
TradePeer sender; TradePeer sender;
if (reOpen) { // re-open can come from either peer if (reOpen) { // re-open can come from either peer
@ -572,8 +575,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
} }
// update multisig hex // update opener's multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender;
if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex());
// add chat message with price info // add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
@ -599,7 +603,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (trade.isArbitrator()) { if (trade.isArbitrator()) {
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker(); TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress()); if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex());
} }
tradeManager.requestPersistence(); tradeManager.requestPersistence();
errorMessage = null; errorMessage = null;

View File

@ -41,9 +41,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
public class DisputeValidation { public class DisputeValidation {
public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException { public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException {
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId()); if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract"); if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract");
if (dispute.getBuyerPaymentAccountPayload() != null) {
if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract");
}
} }
public static void validateDisputeData(Dispute dispute) throws ValidationException { public static void validateDisputeData(Dispute dispute) throws ValidationException {

View File

@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage; import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Contract; import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
@ -177,6 +176,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// remove disputes opened by arbitrator, which is not allowed // remove disputes opened by arbitrator, which is not allowed
Set<Dispute> toRemoves = new HashSet<>(); Set<Dispute> toRemoves = new HashSet<>();
List<Dispute> disputes = getDisputeList().getList(); List<Dispute> disputes = getDisputeList().getList();
synchronized (disputes) {
for (Dispute dispute : disputes) { for (Dispute dispute : disputes) {
// get dispute's trade // get dispute's trade
@ -191,6 +191,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
toRemoves.add(dispute); toRemoves.add(dispute);
} }
} }
}
for (Dispute toRemove : toRemoves) { for (Dispute toRemove : toRemoves) {
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId()); log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
getDisputeList().remove(toRemove); getDisputeList().remove(toRemove);
@ -464,14 +465,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// check daemon connection // check daemon connection
trade.verifyDaemonConnection(); trade.verifyDaemonConnection();
// adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
trade.setPayoutTxHex(null);
}
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
if (trade.getPayoutTxHex() == null) { if (trade.getPayoutTxHex() == null) {
try { try {

View File

@ -55,9 +55,11 @@ public final class MediationDisputeList extends DisputeList<Dispute> {
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
} }
}
public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto,
CoreProtoResolver coreProtoResolver) { CoreProtoResolver coreProtoResolver) {

View File

@ -196,8 +196,8 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} }
} else { } else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId); Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
} }
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);

View File

@ -34,7 +34,7 @@ import java.util.Optional;
public final class DisputeOpenedMessage extends DisputeMessage { public final class DisputeOpenedMessage extends DisputeMessage {
private final Dispute dispute; private final Dispute dispute;
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex; private final String openerUpdatedMultisigHex;
private final PaymentSentMessage paymentSentMessage; private final PaymentSentMessage paymentSentMessage;
public DisputeOpenedMessage(Dispute dispute, public DisputeOpenedMessage(Dispute dispute,
@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
super(messageVersion, uid, supportType); super(messageVersion, uid, supportType);
this.dispute = dispute; this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex; this.openerUpdatedMultisigHex = updatedMultisigHex;
this.paymentSentMessage = paymentSentMessage; this.paymentSentMessage = paymentSentMessage;
} }
@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
.setDispute(dispute.toProtoMessage()) .setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType)) .setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex); .setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex);
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
} }
@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
SupportType.fromProto(proto.getType()), SupportType.fromProto(proto.getType()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()),
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
} }
@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n DisputeOpenedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion + ",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType + ",\n supportType=" + supportType +
",\n updatedMultisigHex=" + updatedMultisigHex + ",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex +
",\n paymentSentMessage=" + paymentSentMessage + ",\n paymentSentMessage=" + paymentSentMessage +
"\n} " + super.toString(); "\n} " + super.toString();
} }

View File

@ -58,10 +58,11 @@ public final class RefundDisputeList extends DisputeList<Dispute> {
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND")); forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND"));
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
} }
}
public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto,
CoreProtoResolver coreProtoResolver) { CoreProtoResolver coreProtoResolver) {

View File

@ -196,8 +196,8 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
tradeManager.requestPersistence(); tradeManager.requestPersistence();
} }
} else { } else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId); Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
} }
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
@ -205,8 +205,8 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
if (tradeManager.getOpenTrade(tradeId).isPresent()) { if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
} else { } else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId); Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
} }
requestPersistence(); requestPersistence();

View File

@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.UUID; import java.util.UUID;
import javax.annotation.Nullable;
/** /**
* Trade in the context of an arbitrator. * Trade in the context of an arbitrator.
*/ */
@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade {
String uid, String uid,
NodeAddress makerNodeAddress, NodeAddress makerNodeAddress,
NodeAddress takerNodeAddress, NodeAddress takerNodeAddress,
NodeAddress arbitratorNodeAddress) { NodeAddress arbitratorNodeAddress,
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress); @Nullable String challenge) {
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge);
} }
@Override @Override
@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade {
uid, uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
proto, proto,
coreProtoResolver); coreProtoResolver);
} }

View File

@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.UUID; import java.util.UUID;
import javax.annotation.Nullable;
@Slf4j @Slf4j
public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
String uid, String uid,
NodeAddress makerNodeAddress, NodeAddress makerNodeAddress,
NodeAddress takerNodeAddress, NodeAddress takerNodeAddress,
NodeAddress arbitratorNodeAddress) { NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer, super(offer,
tradeAmount, tradeAmount,
tradePrice, tradePrice,
@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
uid, uid,
makerNodeAddress, makerNodeAddress,
takerNodeAddress, takerNodeAddress,
arbitratorNodeAddress); arbitratorNodeAddress,
challenge);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
uid, uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
trade.setPrice(proto.getPrice()); trade.setPrice(proto.getPrice());

Some files were not shown because too many files have changed in this diff Show More