Compare commits

..

212 commits

Author SHA1 Message Date
Robbie Blaine
29b79f72fc
Migrate MacOS 13 to 15-intel (#1968) 2025-09-21 00:20:03 -04:00
woodser
0a43b2b2bc
recover from import multisig needed error (#1965) 2025-09-18 09:07:52 -04:00
woodser
f711bd5084
recover if offer funding, deposit, or payout txs are invalidated (#1962) 2025-09-18 05:32:37 -04:00
woodser
2bc877feba create payout address entry for unsigned offer if needed 2025-09-15 17:18:52 -04:00
woodser
e48225fec9 stop polling trade wallet on force restart 2025-09-15 17:18:52 -04:00
woodser
414b10470a initialize all trades together with random delays 2025-09-15 17:18:52 -04:00
woodser
a67e3d3dab
set height on trade initialization (#1960)
* set height on trade initialization

* avoid warning about assuming payouts finalized
2025-09-15 14:00:44 -04:00
woodser
298f48e6f6 always sync wallets with progress 2025-09-15 12:31:19 -04:00
woodser
eb776ea296 fix dropped messages on startup before ready 2025-09-15 12:31:19 -04:00
woodser
6bc74d2ee8 fix init trade progress by synchronizing on notifier 2025-09-15 03:55:33 -04:00
woodser
777cbfdc0c notify balance update after releasing wallet lock 2025-09-15 03:55:33 -04:00
woodser
812dcf27e8 do not revert trade funds on init timeout until error processing 2025-09-15 03:55:33 -04:00
woodser
deb92b71b2
fix npe on startup by caching non-pool txs if necessary (#1957) 2025-09-15 00:58:58 -04:00
woodser
9aca42578f
synchronize setting and getting trade start time (#1956) 2025-09-14 23:52:30 -04:00
woodser
46ccb9b925
schedule to publish trade stats within 12 hours after restart (#1955) 2025-09-14 17:00:51 -04:00
woodser
1a911fdb9d delete trade wallets after 720 confirmations 2025-09-14 16:58:46 -04:00
woodser
6a22f9c6ae initialize active trades before inactive trades 2025-09-14 16:58:46 -04:00
woodser
fe7b949c87
only save wallet on sync with progress if responsive (#1954) 2025-09-14 15:36:28 -04:00
woodser
f079ecaa82
bump version to v1.2.1 (#1952) 2025-09-14 09:12:29 -04:00
woodser
dd177d98cb
update public xmr nodes (#1910) 2025-09-14 07:50:59 -04:00
woodser
35418e5290
handle unexpected errors due to reorgs (#1909)
- show disclaimer until 30 confirmations to send payment
- trade period starts at 30 confirmations
- do not delete multisig wallet until payout has 60 confirmations
- recover from stale multisig state via payment received nacks
- fix a bug which re-signs stale payout tx
- add handling for failed or missing deposit and payout txs
- buyer can process payout tx to main wallet
- do not process outdated payment received messages
- poll trade wallet on startup without network calls 
- recover missing wallet data on create and process dispute payout
- arbitrator nacks dispute request if payout already published
- recover if offer funding tx is invalidated
2025-09-14 07:49:45 -04:00
woodser
7fa633273c
add makefile config for buyer and seller mainnet wallet (#1950) 2025-09-09 09:24:29 -04:00
woodser
b4789ebc9e format message on error connecting to monero network 2025-09-09 09:21:35 -04:00
woodser
01d56f74b3 connection service switches connection after max attempts 2025-09-09 09:21:35 -04:00
woodser
da14132b0e fix connection error color mismatch 2025-09-09 09:21:35 -04:00
woodser
729b5a3a74
only log possible dos attack if >5 throttled messages (#1946) 2025-09-09 09:21:13 -04:00
woodser
171acd5221
synchronize broadcaster requests and handlers (#1925) 2025-09-09 09:21:05 -04:00
woodser
5f3e366920
fix deposit tx not found error when confirmed before relayed (#1942) 2025-09-05 06:35:34 -04:00
woodser
0c1e4f31af
improve reliability of requesting persistence and persisting trade state (#1921) 2025-08-31 09:55:34 -04:00
woodser
afe4ba3ad8
fix reactivation of mutable offer tab on return (#1933) 2025-08-31 09:52:02 -04:00
woodser
6c89c61dd1
fix distortions when returning to create offer screen (#1932) 2025-08-31 09:51:50 -04:00
woodser
d6ff436656
fix error reopening clone offer tab after changing tabs (#1930) 2025-08-31 09:51:33 -04:00
woodser
cfc62aa9ae add random delay to publish trade statistics on startup within 24 hours 2025-08-31 09:43:58 -04:00
woodser
723d5cc1dd publish trade stat after random delay in ms 2025-08-31 09:43:58 -04:00
woodser
19ee6d4343
add bottom padding for copy, clone, and edit offer views (#1935) 2025-08-31 09:42:52 -04:00
woodser
55c02a53d3
fix outdated currency shown in create offer like this (#1931) 2025-08-31 09:42:25 -04:00
woodser
f7ec36fd70 set max popup height to 730 2025-08-25 11:28:13 -04:00
woodser
00737b6e79 do not send message contents on init trade nack 2025-08-25 11:28:13 -04:00
woodser
b0e1d5b4ce do not remove maker offer on nack due to send message error 2025-08-25 11:28:13 -04:00
The rise of Agorism
5b08c66174
Add Agorise pricenode (#1915) 2025-08-19 08:54:41 -04:00
наб
3067609c64
Supply custom .desktop file for .deb builds to fix icons under GNOME (#1917) 2025-08-14 21:19:08 -04:00
woodser
2045375f3e
remove USDT-TRC20 from default main currencies (#1918) 2025-08-14 20:52:29 -04:00
woodser
348bfb7f38
fix NPE reading wallet height before initialized (#1916) 2025-08-14 13:26:51 -04:00
woodser
b76a556487
update instructions to use v1.2.0 (#1914) 2025-08-14 12:29:27 -04:00
woodser
4ca091940e
remove boldsuck price node (#1913) 2025-08-14 10:48:13 -04:00
woodser
9e8c2cd184
update app copyright to 2025 (#1912) 2025-08-14 10:44:22 -04:00
woodser
7bf9475585 support Dogecoin (DOGE) 2025-08-11 10:32:51 -04:00
woodser
878cbb86ce support Tron (TRX) 2025-08-11 10:32:51 -04:00
woodser
b0446c637f support Solana (SOL) 2025-08-11 10:32:51 -04:00
woodser
0dc67f06c4 support Cardano (ADA) 2025-08-11 10:32:51 -04:00
woodser
7298a6373a support Ripple (XRP) 2025-08-11 10:32:51 -04:00
woodser
6537586976
fix broken link to tor bridges (#1908) 2025-08-11 10:32:39 -04:00
woodser
2844337c09
fix upgrading cloned offers after protocol update (#1902) 2025-08-11 09:17:48 -04:00
Robbie Blaine
6fd5772308
Hotfix: Create local share dir for Codacy Report (#1904) 2025-08-08 12:59:14 -04:00
Robbie Blaine
3168c0174f Windows 2025
* Pre-emptively upgrade Windows runner from `server-2022` to
  `server-2025`
* `windows-latest` is automatically upgrading to `server-2025` on
  September 2, 2025 (actions/runner-images#12677)
2025-08-08 12:22:29 -04:00
Robbie Blaine
cd3fb8f6fc Upgrade Github Runners to Ubuntu 24.04
Fix `FileNotFoundException` errors when running tests on Ubuntu 24.04.
Tests were failing because the user data directory did not exist in the
new GitHub Ubuntu-24.04 runner environment.

The change:
- Updates all GitHub Actions workflows to use `ubuntu-24.04`
- Creates `~/.local/share` directory in CI pipeline to ensure the user
  data directory exists during tests

This resolves the blocking issue that prevented the Ubuntu upgrade by
ensuring the required directory structure exists in the CI environment.
2025-08-08 12:22:29 -04:00
woodser
4aab94ac26
enable option to allocate offer funds by default (#1901) 2025-08-07 14:38:23 -04:00
atsamd21
a467caa135
Fix label for account number (#1903) 2025-08-07 14:33:17 -04:00
Robbie Blaine
5226574330
Add aarch64 support to CI (#1743)
Extend CI workflow to build for both `x86_64` and `aarch64` architectures:
- Add ARM variants for Ubuntu and macOS in the build matrix
- Skip tests on `aarch64` builds with `-x test` flag as JavaFX 21.x.x
  doesn't currently support the ARM64 architecture
- Update artifact naming to include architecture identifiers
- Configure architecture-specific build paths and release outputs

This change enables native builds for ARM-based systems like Apple Silicon
Macs and ARM servers, improving performance for users on these platforms
while maintaining full test coverage on `x86_64` where supported.

Make localnet cache OS-specific

Prevent Error: Failed to CreateArtifact: Received non-retryable
error: Failed request: (409) Conflict: an artifact with this name
already exists on the workflow run

Add a warning about `linux/aarch64` tests being skipped

Simplify GitHub Actions workflow conditions

Replace verbose condition syntax with simpler `runner.os` expressions to
improve readability and maintainability. This change:

- Uses `runner.os` instead of specific `matrix.os` version strings
- Removes unnecessary `${{ }}` expression wrappers in if conditions
- Consolidates OS-specific conditions (e.g. `ubuntu-22.04` or
  `ubuntu-22.04-arm`) into simpler checks (`runner.os == 'Linux'`)

Fix JavaFX loading on Mac ARM64 architecture

JavaFX 21.0.2 supports Mac ARM64 (Apple Silicon) but was failing to
load with `UnsatisfiedLinkError` because the build system wasn't
detecting ARM architecture properly.

This change allows the build system to correctly identify Mac ARM64
systems and use the appropriate JavaFX libraries, resolving the
compatibility issues when running on Apple Silicon Macs.

Split Build and Package Installer steps

Cleaner CI output, split Build and Package installer step into two steps
2025-08-06 15:12:57 -04:00
woodser
62228625cb add monero nodes for public mainnet 2025-07-30 08:03:28 -04:00
woodser
d09fe78c62 skip switching connection when outdated and main wallet disconnected 2025-07-30 08:03:28 -04:00
woodser
4497632e77 fix bug to apply tor proxy uri with monerod onion 2025-07-30 08:03:28 -04:00
woodser
c8fab5fb37
fixes when a deposit transaction does not confirm (#1892) 2025-07-30 07:57:02 -04:00
woodser
19acab4e25
waiting for seller label spans multiple columns (#1891) 2025-07-30 07:56:33 -04:00
woodser
10ef8aab2b
increase minimum height for open trades table (#1890)
set minimum height for open trades table based on num trades
2025-07-30 07:56:20 -04:00
woodser
f96f7d2b96
refactor filtering for failed trade list item (#1889) 2025-07-30 07:55:49 -04:00
woodser
d20ad82a9f improve error handling on create dispute payout tx 2025-07-30 07:54:29 -04:00
woodser
40aacf4672 request connection switch every other attempt 2025-07-30 07:54:29 -04:00
woodser
d575f384ef skip connection switch on illegal error syncing wallet 2025-07-30 07:54:29 -04:00
woodser
ef0f841f90
fixes to resend payment received messages up to 2 months (#1887) 2025-07-30 07:49:52 -04:00
woodser
017d8d52ba
fix expected case of unknown monero peer count (#1885) 2025-07-23 20:27:39 -04:00
woodser
0ba3fb02fc
update documentation to set public keys (#1884) 2025-07-23 16:13:33 -04:00
woodser
2ff149b1eb
update public keys with dev privileges on stagenet (#1883) 2025-07-23 15:59:04 -04:00
woodser
866aaac51d
update to monero-java v0.8.38 (#1882) 2025-07-23 15:56:37 -04:00
woodser
4b2e294272
update to monero-project v0.18.4.1 binaries (#1880) 2025-07-23 15:56:29 -04:00
woodser
6925117f12
fix CI build by using continuous tag for AppImage tool (#1881) 2025-07-23 13:18:18 -04:00
woodser
bf1727bda6
bump version to 1.2.0 (#1878) 2025-07-21 09:59:19 -04:00
woodser
1276f83485
hide error popups when errors are cleared (#1877) 2025-07-21 09:59:09 -04:00
woodser
ffadc09712
use separate fees for crypto and traditional payment methods (#1865) 2025-07-21 09:58:56 -04:00
woodser
fd2c0f335f
set minimum trade amount to 0.05 XMR (#1857) 2025-07-21 09:57:54 -04:00
woodser
3680e1d4ee
set penalty fee to 25% of security deposit (#1844) 2025-07-21 09:57:40 -04:00
woodser
a4d744aa53
remove inverted crypto prices; always use price per XMR (base/quote) (#1821) 2025-07-21 09:56:57 -04:00
woodser
51d40d73a7
randomize trade amount +-10%, price +-1%, date within 48 hours (fork) (#1815) 2025-07-21 09:56:04 -04:00
woodser
79dbe34359 do not show duplicate popups 2025-07-20 16:35:17 -04:00
woodser
9f55c5d648 show error popup when unrestricted monerod has 0 peers 2025-07-20 16:35:17 -04:00
woodser
f4d2646cf3 rename daemon to monerod 2025-07-20 16:35:17 -04:00
woodser
68ee80c6ef
reverse buy and sell xmr panels in market view (#1876) 2025-07-20 16:34:44 -04:00
woodser
45e54b69e8
fix sorting of trigger price icon with open offer row (#1874) 2025-07-20 16:34:25 -04:00
woodser
7fb7504046
revert 5ca44c4 payment method mappings and add note for new methods only (#1873) 2025-07-20 16:33:50 -04:00
woodser
f722ca4cd4
add validation and self correct recorded security deposits (#1872) 2025-07-20 16:33:35 -04:00
woodser
0b04d34909
set offer state to invalid on failed validation (#1871) 2025-07-19 08:01:35 -04:00
woodser
dd1fced7fb
cannot set trigger price for fixed price offers over api (#1870) 2025-07-19 08:01:19 -04:00
woodser
071659aec1
fix amount adjustment when creating new offer (#1868) 2025-07-19 08:00:46 -04:00
woodser
83e1e56efb
update tails readme to use installer instead of zip archive (#1869) 2025-07-18 17:13:00 -04:00
woodser
922fb6df24
add preference to clear sensitive data after days (#1859) 2025-07-18 12:56:25 -04:00
woodser
ca1dde03f5
peers publish deposit tx for redundancy (#1839) 2025-07-15 06:35:37 -04:00
woodser
ccbe2d2fe1
add logs when trade taken and payment account decrypted (#1861) 2025-07-14 11:56:11 -04:00
woodser
b0ecc628e3
remove 1px border on bottom of navigation bar (#1862) 2025-07-14 11:54:13 -04:00
woodser
3f5dc9c077
darken primary blue color (#1863) 2025-07-14 11:54:01 -04:00
woodser
12483df705
sort traditional currencies after fiat currencies in combined pull down (#1864) 2025-07-14 11:53:50 -04:00
woodser
fd664d1d30
remove min. non-dust from preferences (#1860) 2025-07-14 08:37:44 -04:00
woodser
dff7e88428
fix min amount creating offer after setting volume (#1858) 2025-07-13 10:28:15 -04:00
woodser
4627960c05
recover trade funds via dispute if deposit transaction dropped (#1838) 2025-07-12 10:44:10 -04:00
woodser
10347ae488
restore last combo box selection on focus out 2025-07-12 10:43:23 -04:00
woodser
8f505ab17b enable volume input when taking range offer 2025-07-12 09:11:25 -04:00
woodser
0cf34f3170 round offer amounts within min and max range 2025-07-12 09:11:25 -04:00
woodser
953157c965
set vertical spacing of pending trades view to 10 for consistency 2025-07-12 08:10:34 -04:00
woodser
68005c4daa
widen offer details window for confirm and cancel buttons 2025-07-11 10:36:57 -04:00
woodser
181bb2aa26
remove 'revert tx' column from transactions view 2025-07-11 09:44:26 -04:00
woodser
a2f54215de
always show tx withdraw window 2025-07-09 06:55:26 -04:00
woodser
100b2136a7
fix error loading interac e-transfer offers 2025-07-09 06:55:09 -04:00
woodser
da9cf540e6
instruct to build v1.1.2 2025-07-09 06:54:42 -04:00
woodser
19b8aaaf23
transfer open offer's challenge when upgraded 2025-07-08 10:31:56 -04:00
woodser
0b4f5bf4ed
adjust payment accounts list height dynamically 2025-07-08 07:11:24 -04:00
woodser
75b8eb1dc9
provide trade start time, duration, and deadline in grpc api 2025-07-08 07:08:30 -04:00
woodser
384e771712
fix vertical alignment of fixed price swap arrows 2025-07-07 20:45:19 -04:00
woodser
ea506ecaf2
widen currency text box when creating offer 2025-07-07 20:44:55 -04:00
woodser
ed25756ae8 use stackpane for currency icons 2025-07-07 19:29:51 -04:00
woodser
55032f94e7 use logo for fiat currencies 2025-07-07 19:29:51 -04:00
woodser
61122493b5
remove unused progress bar below network info 2025-07-05 10:56:36 -04:00
woodser
d8bd91f959
always use 'currency name (code)' format 2025-07-05 10:56:25 -04:00
woodser
da17bcc76d
fix alignment of market price pct when taking offer 2025-07-05 10:56:12 -04:00
woodser
93f6337e6a
fix opening matrix.to link under support button by escaping 2025-07-05 10:56:02 -04:00
jermanuts
19d4df8b2e
Add translation guide to CONTRIBUTING.md 2025-07-03 10:45:29 -04:00
woodser
c6ef499ced fix vertical alignment of text field with icon 2025-07-03 09:30:11 -04:00
woodser
e089a6f2a4 wallet poll requests connection changes off thread to avoid deadlock 2025-07-03 07:16:05 -04:00
woodser
5ca44c40cf add payment methods to trade statistics PaymentMethodMapper 2025-07-02 10:02:04 -04:00
woodser
e3d7499004 fix vertical alignment of price column in offer book view 2025-07-02 08:31:07 -04:00
woodser
4a6043841a support US postal money order accounts over grpc api 2025-07-02 08:30:55 -04:00
woodser
9932ae84f4 support interac e-transfer payment accounts over grpc api 2025-07-02 08:30:55 -04:00
woodser
a8ba57f444 support ach transfer payment account over grpc api 2025-07-02 08:30:55 -04:00
woodser
d19610b93a support amazon e-gift card payment account over grpc api 2025-07-02 08:30:55 -04:00
woodser
3e9a55d784 support wise usd payment account over grpc api 2025-07-02 08:30:55 -04:00
woodser
59e509d3e3 support swish payment account over grpc api 2025-07-02 08:30:55 -04:00
woodser
bd40067deb support alipay grpc api 2025-07-02 08:30:55 -04:00
woodser
e3e0256f7a support wechat pay grpc api 2025-07-02 08:30:55 -04:00
woodser
33c9628df3 use BigInteger for average chart calculations 2025-07-01 09:44:41 -04:00
woodser
3546d3d931 fix average calculation in trade charts view 2025-07-01 09:44:41 -04:00
woodser
8cca2cbb52
do not color currency code in offer book volume column 2025-06-28 10:17:38 -04:00
jermanuts
b289a256ee
fix donation permalink 2025-06-23 11:42:14 -04:00
woodser
5141574d70
fix error on export table columns 2025-06-22 08:38:43 -04:00
woodser
32148e7440
fix translations by automatically escaping single quotes, remove escapes 2025-06-19 09:03:51 -04:00
woodser
b82d6c3004
widen withdraw amount field 2025-06-19 08:09:43 -04:00
Olexandr88
81f7bac452
added link to build badge 2025-06-18 08:13:22 -04:00
woodser
1875f6a70c
increase contrast of nav balance subtext 2025-06-17 09:56:16 -04:00
woodser
bc8f473b46
adjust spacing when creating new accounts 2025-06-16 09:24:05 -04:00
woodser
2f18a74478
support sweeping funds from grpc api, relay multiple txs 2025-06-13 07:57:02 -04:00
atsamd21
ab5f6c8191
Add disableRateLimits option to daemon (#1803) 2025-06-12 08:16:59 -04:00
woodser
145f0c9cf6 make select backup location button green 2025-06-10 09:08:00 -04:00
woodser
4f9e39410d make account creation buttons green 2025-06-10 09:08:00 -04:00
woodser
e4e118f70c
rename logos with dark_mode and light_mode postfix 2025-06-09 07:06:43 -04:00
woodser
53e2c5cc24
center the top nav buttons when window is small 2025-06-09 07:06:00 -04:00
woodser
62d5eb4bc3
fix distorted confirm payment sent checkbox in windows dark mode 2025-06-09 07:05:46 -04:00
woodser
183782982c
fix display name of non-fiat traditional currencies 2025-06-09 06:51:59 -04:00
woodser
ee49324fbb
fix divide by zero error opening trade summary with no history 2025-06-08 10:14:27 -04:00
woodser
285335d138
bump version to 1.1.2 2025-06-06 08:24:45 -04:00
woodser
aa1eb70d9a
major update to look and feel of desktop ui (#1733) 2025-06-06 08:18:51 -04:00
woodser
c239f9aac0 make updated multisig hex nullable in dispute closed message 2025-06-05 21:22:29 -04:00
woodser
8ee1bb372b fix hanging of pending offer with scheduled txs 2025-06-05 10:57:40 -04:00
woodser
ea536bb4ee fix npe on startup routine when monero node is not synced 2025-06-05 10:48:58 -04:00
woodser
a4345ae709 arbitrator verifies offers are public xor no deposit from buyer/taker 2025-06-03 21:51:26 -04:00
woodser
33a91cf980 maker recreates reserve tx then cancels offer on trade nacks 2025-06-03 07:42:30 -04:00
woodser
264e5f436e stricter validation for creating private, no deposit offers 2025-06-03 07:42:20 -04:00
woodser
501530d85f read bip39 words once and synchronized 2025-06-03 07:42:20 -04:00
woodser
fa375b3cbd process connection messages on main run thread 2025-06-03 07:42:01 -04:00
woodser
3648c1eb0e skip polling if shut down started after acquiring lock 2025-06-03 07:42:01 -04:00
woodser
98130499a7 fix log of trade miner fee 2025-06-01 08:37:59 -04:00
woodser
9dd011afc8 poll key images in batches 2025-06-01 08:37:59 -04:00
woodser
5b04eb17a2 improve error messages when deposit txs are missing 2025-06-01 08:37:59 -04:00
woodser
dd65cdca13 fix custom amounts for dispute result 2025-06-01 08:37:59 -04:00
woodser
85ee6787cd fix npe on report dispute button with null payment account 2025-06-01 08:37:59 -04:00
woodser
8f310469da fix npe with japan bank account 2025-05-29 17:08:13 -04:00
woodser
5d5eb649c6 log possible DoS attack 2025-05-29 17:08:04 -04:00
woodser
a37654e116 arbitrator transactions show maker and taker fees 2025-05-29 17:08:04 -04:00
woodser
ddab170210
remove currency codes from crypto names 2025-05-28 08:56:34 -04:00
PromptPunksFauxCough
115fa96daf
Update exec.sh to leverage Tails Stream-Isolation (#1746)
https://tails.net/contribute/design/stream_isolation
2025-05-28 06:25:45 -04:00
woodser
4c30e4625b
improve error message when offer's arbitrator is not registered 2025-05-25 08:57:32 -04:00
PromptPunksFauxCough
050e6b907a
Update Qubes/Whonix install for individual files (#1739) 2025-05-12 17:27:39 -04:00
woodser
fe3283f3b0 update donation qr and readme 2025-05-08 07:10:39 -04:00
woodser
c4758d1e4b use default 'password' to authenticate with wallet rpc server 2025-05-07 08:37:24 -04:00
woodser
e3946f3aba move settings tab to last 2025-05-02 06:27:12 -04:00
woodser
c2fbd4b16f update donation address 2025-04-30 12:06:27 -04:00
woodser
c214919aa5 fix concurrent modification in portfolio by locking sequence number map 2025-04-29 08:39:34 -04:00
woodser
81eaeb6df0 bump version to v1.1.1 2025-04-26 19:08:22 -04:00
woodser
ae5ee15a85 instruct to try again on nack 2025-04-26 15:59:39 -04:00
woodser
0b8e43b7a8 preserve old behavior when nack does not include updated multisig 2025-04-26 15:59:39 -04:00
woodser
350aa10839 tolerate miner fees within 5x of each other 2025-04-26 15:59:39 -04:00
woodser
adcd5da431 fix shut down of trade and wallet services in seed node 2025-04-26 07:26:25 -04:00
woodser
58506b02f5 recover from payment received nack with updated multisig info 2025-04-24 21:07:05 -04:00
woodser
8611593a3f do not force restart main wallet on connection change with same config 2025-04-24 21:07:05 -04:00
woodser
bd9c28fafa remove expected warning when wallet is null 2025-04-24 21:07:05 -04:00
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
390 changed files with 9301 additions and 4732 deletions

View file

@ -14,7 +14,18 @@ jobs:
build: build:
strategy: strategy:
matrix: matrix:
os: [ubuntu-22.04, macos-13, windows-latest] os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-15, windows-2025]
include:
- os: ubuntu-24.04
arch: x86_64
- os: ubuntu-24.04-arm
arch: aarch64
- os: macos-15-intel
arch: x86_64
- os: macos-15
arch: aarch64
- os: windows-2025
arch: x86_64
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -27,8 +38,25 @@ jobs:
java-version: '21' java-version: '21'
distribution: 'adopt' distribution: 'adopt'
cache: gradle cache: gradle
- name: Build with Gradle - name: Create local share directory
# ubuntu-24.04 Github runners do not have `~/.local/share` directory by default.
# This causes issues when testing `FileTransferSend`
if: runner.os == 'Linux'
run: mkdir -p ~/.local/share
- name: Build with Gradle with tests
if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }}
run: ./gradlew build --stacktrace --scan run: ./gradlew build --stacktrace --scan
- name: Build with Gradle, skip Desktop tests
# JavaFX `21.x.x` ships with `x86_64` versions of
# `libprism_es2.so` and `libprism_s2.so` shared objects.
# This causes desktop tests to fail on `linux/aarch64`
if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }}
run: |
./gradlew build --stacktrace --scan -x desktop:test
echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \
intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \
shared objects, causing tests to fail. \
This should be revisited when JavaFX is next updated."
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
@ -38,115 +66,131 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
include-hidden-files: true include-hidden-files: true
name: cached-localnet name: cached-localnet-${{ matrix.os }}
path: .localnet path: .localnet
overwrite: true overwrite: true
- name: Install dependencies - name: Install dependencies
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get 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: runner.os == 'Windows'
run: | run: |
Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe
.\wix314.exe /quiet /norestart .\wix314.exe /quiet /norestart
shell: powershell shell: powershell
- name: Build Haveno Installer - name: Build Haveno Installer with tests
if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }}
run: ./gradlew clean build --refresh-keys --refresh-dependencies
working-directory: .
- name: Build Haveno Installer, skip Desktop tests
# JavaFX `21.x.x` ships with `x86_64` versions of
# `libprism_es2.so` and `libprism_s2.so` shared objects.
# This causes desktop tests to fail on `linux/aarch64`
if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }}
run: | run: |
./gradlew clean build --refresh-keys --refresh-dependencies ./gradlew clean build --refresh-keys --refresh-dependencies -x desktop:test
./gradlew packageInstallers echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \
intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \
shared objects, causing tests to fail. \
This should be revisited when JavaFX is next updated."
working-directory: .
- name: Package Haveno Installer
run: ./gradlew packageInstallers
working-directory: . working-directory: .
# get version from jar # get version from jar
- name: Set Version Unix - name: Set Version Unix
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} if: runner.os != 'Windows'
run: | run: |
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//') export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Set Version Windows - name: Set Version Windows
if: ${{ matrix.os == 'windows-latest' }} if: runner.os == 'Windows'
run: | run: |
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', '' $VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
shell: powershell shell: powershell
- name: Move Release Files for Linux - name: Move Release Files for Linux
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
run: | run: |
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
mkdir ${{ github.workspace }}/release-linux-appimage mkdir ${{ github.workspace }}/release-linux-appimage
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.rpm
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.deb
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.flatpak
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.AppImage
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-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
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256
shell: bash shell: bash
- name: Move Release Files for macOS - name: Move Release Files for macOS
if: ${{ matrix.os == 'macos-13' }} if: runner.os == 'MacOS'
run: | run: |
mkdir ${{ github.workspace }}/release-macos mkdir ${{ github.workspace }}/release-macos-${{ matrix.arch }}
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-${{ matrix.arch }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-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-${{ matrix.arch }}
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-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: runner.os == 'Windows'
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-${{ matrix.arch }}-installer.exe
Copy-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 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 # Windows artifacts
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "Windows artifacts" name: "Windows artifacts"
if: ${{ matrix.os == 'windows-latest' }} if: runner.os == 'Windows'
with: with:
name: haveno-windows name: haveno-windows-${{ matrix.arch }}
path: ${{ github.workspace }}/release-windows path: ${{ github.workspace }}/release-windows
# macos
# macOS artifacts
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "macOS artifacts" name: "macOS artifacts"
if: ${{ matrix.os == 'macos-13' }} if: runner.os == 'MacOS'
with: with:
name: haveno-macos name: haveno-macos-${{ matrix.arch }}
path: ${{ github.workspace }}/release-macos path: ${{ github.workspace }}/release-macos-${{ matrix.arch }}
# linux
# Linux artifacts
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "Linux - deb artifact" name: "Linux - deb artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
with: with:
name: haveno-linux-deb name: haveno-linux-${{ matrix.arch }}-deb
path: ${{ github.workspace }}/release-linux-deb path: ${{ github.workspace }}/release-linux-deb
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "Linux - rpm artifact" name: "Linux - rpm artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
with: with:
name: haveno-linux-rpm name: haveno-linux-${{ matrix.arch }}-rpm
path: ${{ github.workspace }}/release-linux-rpm path: ${{ github.workspace }}/release-linux-rpm
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "Linux - AppImage artifact" name: "Linux - AppImage artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
with: with:
name: haveno-linux-appimage name: haveno-linux-${{ matrix.arch }}-appimage
path: ${{ github.workspace }}/release-linux-appimage path: ${{ github.workspace }}/release-linux-appimage
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: "Linux - flatpak artifact" name: "Linux - flatpak artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }} if: runner.os == 'Linux'
with: with:
name: haveno-linux-flatpak name: haveno-linux-${{ matrix.arch }}-flatpak
path: ${{ github.workspace }}/release-linux-flatpak path: ${{ github.workspace }}/release-linux-flatpak
- name: Release - name: Release
@ -154,14 +198,30 @@ jobs:
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
# Linux x86_64
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb ${{ 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-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-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak ${{ 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 }}/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 # Linux aarch64
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-aarch64-installer.deb
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-aarch64-installer.rpm
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-aarch64.AppImage
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-aarch64.flatpak
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-aarch64-SNAPSHOT-all.jar.SHA-256
# macOS x86_64
${{ github.workspace }}/release-macos-x86_64/haveno-v${{ env.VERSION }}-macos-x86_64-installer.dmg
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-x86_64-SNAPSHOT-all.jar.SHA-256
# macOS aarch64
${{ github.workspace }}/release-macos-aarch64/haveno-v${{ env.VERSION }}-macos-aarch64-installer.dmg
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-aarch64-SNAPSHOT-all.jar.SHA-256
# Windows
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-x86_64-installer.exe
${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
# https://git-scm.com/docs/git-tag - git-tag Docu # https://git-scm.com/docs/git-tag - git-tag Docu

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-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -19,6 +19,11 @@ jobs:
java-version: '21' java-version: '21'
distribution: 'adopt' distribution: 'adopt'
- name: Create local share directory
# ubuntu-24.04 Github runners do not have `~/.local/share` directory by default.
# This causes issues when testing `FileTransferSend`
run: mkdir -p ~/.local/share
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar

View file

@ -18,7 +18,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
permissions: permissions:
actions: read actions: read
contents: read contents: read

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
issueLabeled: issueLabeled:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.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

@ -70,11 +70,12 @@ 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 \ --rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod2-local: monerod2-local:
./.localnet/monerod \ ./.localnet/monerod \
@ -90,11 +91,12 @@ 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 \ --rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod3-local: monerod3-local:
./.localnet/monerod \ ./.localnet/monerod \
@ -110,11 +112,12 @@ 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 \ --rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
#--proxy 127.0.0.1:49775 \ #--proxy 127.0.0.1:49775 \
@ -440,6 +443,9 @@ monerod:
./.localnet/monerod \ ./.localnet/monerod \
--bootstrap-daemon-address auto \ --bootstrap-daemon-address auto \
--rpc-access-control-origins http://localhost:8080 \ --rpc-access-control-origins http://localhost:8080 \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
seednode: seednode:
./haveno-seednode$(APP_EXT) \ ./haveno-seednode$(APP_EXT) \
@ -485,6 +491,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 \
@ -570,3 +601,19 @@ user3-desktop-mainnet:
--apiPort=1204 \ --apiPort=1204 \
--useNativeXmrWallet=false \ --useNativeXmrWallet=false \
--ignoreLocalXmrNode=false \ --ignoreLocalXmrNode=false \
buyer-wallet-mainnet:
./.localnet/monero-wallet-rpc \
--daemon-address http://localhost:18081 \
--rpc-bind-port 18084 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
seller-wallet-mainnet:
./.localnet/monero-wallet-rpc \
--daemon-address http://localhost:18081 \
--rpc-bind-port 18085 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \

View file

@ -1,7 +1,7 @@
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo"> <img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)](https://github.com/haveno-dex/haveno/actions)
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) [![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
[![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex) [![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex)
[![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md) [![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
@ -67,19 +67,17 @@ 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-and-sponsorships) fund development bounties. If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties.
## Bounties ## Bounties
To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md). To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
## Support and sponsorships ## Support
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 donating to the project:
<p> <p align="center">
<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>47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB</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.

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.getDefaultSecurityDepositAsPercent; import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct;
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;
@ -158,7 +158,7 @@ public class MethodTest extends ApiTestCase {
} }
public static final Supplier<Double> defaultSecurityDepositPct = () -> { public static final Supplier<Double> defaultSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent()); var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct());
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

@ -0,0 +1,106 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
/**
* Validates a Shelley-era mainnet Cardano address.
*/
public class CardanoAddressValidator extends RegexAddressValidator {
private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
private static final int BECH32_CONST = 1;
private static final int BECH32M_CONST = 0x2bc830a3;
private static final int MAX_LEN = 104; // bech32 / bech32m max for Cardano
public CardanoAddressValidator() {
super("^addr1[0-9a-z]{20,98}$");
}
public CardanoAddressValidator(String errorMessageI18nKey) {
super("^addr1[0-9a-z]{20,98}$", errorMessageI18nKey);
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidShelleyMainnet(address)) {
return AddressValidationResult.invalidStructure();
}
return super.validate(address);
}
/**
* Checks if the given address is a valid Shelley-era mainnet Cardano address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidShelleyMainnet(String addr) {
if (addr == null) return false;
String lower = addr.toLowerCase();
// must start addr1 and not be absurdly long
if (!lower.startsWith("addr1") || lower.length() > MAX_LEN) return false;
int sep = lower.lastIndexOf('1');
if (sep < 1) return false; // no separator or empty HRP
String hrp = lower.substring(0, sep);
if (!"addr".equals(hrp)) return false; // mainnet only
String dataPart = lower.substring(sep + 1);
if (dataPart.length() < 6) return false; // checksum is 6 chars minimum
int[] data = new int[dataPart.length()];
for (int i = 0; i < dataPart.length(); i++) {
int v = CHARSET.indexOf(dataPart.charAt(i));
if (v == -1) return false;
data[i] = v;
}
int[] hrpExp = hrpExpand(hrp);
int[] combined = new int[hrpExp.length + data.length];
System.arraycopy(hrpExp, 0, combined, 0, hrpExp.length);
System.arraycopy(data, 0, combined, hrpExp.length, data.length);
int chk = polymod(combined);
return chk == BECH32_CONST || chk == BECH32M_CONST; // accept either legacy Bech32 (1) or Bech32m (0x2bc830a3)
}
private static int[] hrpExpand(String hrp) {
int[] ret = new int[hrp.length() * 2 + 1];
int idx = 0;
for (char c : hrp.toCharArray()) ret[idx++] = c >> 5;
ret[idx++] = 0;
for (char c : hrp.toCharArray()) ret[idx++] = c & 31;
return ret;
}
private static int polymod(int[] values) {
int chk = 1;
int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
for (int v : values) {
int b = chk >>> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (int i = 0; i < 5; i++) {
if (((b >>> i) & 1) != 0) chk ^= GEN[i];
}
}
return chk;
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
/**
* Validates a Ripple address using a regular expression.
*/
public class RippleAddressValidator extends RegexAddressValidator {
public RippleAddressValidator() {
super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$");
}
public RippleAddressValidator(String errorMessageI18nKey) {
super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$", errorMessageI18nKey);
}
}

View file

@ -0,0 +1,94 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
import java.math.BigInteger;
/**
* Validates a Solana address.
*/
public class SolanaAddressValidator implements AddressValidator {
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public SolanaAddressValidator() {
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidSolanaAddress(address)) {
return AddressValidationResult.invalidStructure();
}
return AddressValidationResult.validAddress();
}
/**
* Checks if the given address is a valid Solana address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidSolanaAddress(String address) {
if (address == null) return false;
if (address.length() < 32 || address.length() > 44) return false; // typical Solana length range
// Check all chars are base58 valid
for (char c : address.toCharArray()) {
if (BASE58_ALPHABET.indexOf(c) == -1) return false;
}
// Decode from base58 and ensure exactly 32 bytes
byte[] decoded = decodeBase58(address);
return decoded != null && decoded.length == 32;
}
private static byte[] decodeBase58(String input) {
BigInteger num = BigInteger.ZERO;
BigInteger base = BigInteger.valueOf(58);
for (char c : input.toCharArray()) {
int digit = BASE58_ALPHABET.indexOf(c);
if (digit < 0) return null; // invalid char
num = num.multiply(base).add(BigInteger.valueOf(digit));
}
// Convert BigInteger to byte array
byte[] bytes = num.toByteArray();
// Remove sign byte if present
if (bytes.length > 1 && bytes[0] == 0) {
byte[] tmp = new byte[bytes.length - 1];
System.arraycopy(bytes, 1, tmp, 0, tmp.length);
bytes = tmp;
}
// Count leading '1's and add leading zero bytes
int leadingZeros = 0;
for (char c : input.toCharArray()) {
if (c == '1') leadingZeros++;
else break;
}
byte[] result = new byte[leadingZeros + bytes.length];
System.arraycopy(bytes, 0, result, leadingZeros, bytes.length);
return result;
}
}

View file

@ -0,0 +1,104 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* Validates a Tron address.
*/
public class TronAddressValidator implements AddressValidator {
private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final byte MAINNET_PREFIX = 0x41;
public TronAddressValidator() {
}
@Override
public AddressValidationResult validate(String address) {
if (!isValidTronAddress(address)) {
return AddressValidationResult.invalidStructure();
}
return AddressValidationResult.validAddress();
}
/**
* Checks if the given address is a valid Solana address.
*
* This code is AI-generated and has been tested with a variety of addresses.
*
* @param addr the address to validate
* @return true if the address is valid, false otherwise
*/
private static boolean isValidTronAddress(String address) {
if (address == null || address.length() != 34) return false;
byte[] decoded = decodeBase58(address);
if (decoded == null || decoded.length != 25) return false; // 21 bytes data + 4 bytes checksum
// Check checksum
byte[] data = Arrays.copyOfRange(decoded, 0, 21);
byte[] checksum = Arrays.copyOfRange(decoded, 21, 25);
byte[] calculatedChecksum = Arrays.copyOfRange(doubleSHA256(data), 0, 4);
if (!Arrays.equals(checksum, calculatedChecksum)) return false;
// Check mainnet prefix
return data[0] == MAINNET_PREFIX;
}
private static byte[] decodeBase58(String input) {
BigInteger num = BigInteger.ZERO;
BigInteger base = BigInteger.valueOf(58);
for (char c : input.toCharArray()) {
int digit = BASE58_ALPHABET.indexOf(c);
if (digit < 0) return null;
num = num.multiply(base).add(BigInteger.valueOf(digit));
}
// Convert BigInteger to byte array
byte[] bytes = num.toByteArray();
if (bytes.length > 1 && bytes[0] == 0) {
bytes = Arrays.copyOfRange(bytes, 1, bytes.length);
}
// Add leading zero bytes for '1's
int leadingZeros = 0;
for (char c : input.toCharArray()) {
if (c == '1') leadingZeros++;
else break;
}
byte[] result = new byte[leadingZeros + bytes.length];
System.arraycopy(bytes, 0, result, leadingZeros, bytes.length);
return result;
}
private static byte[] doubleSHA256(byte[] data) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(sha256.digest(data));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.CardanoAddressValidator;
import haveno.asset.Coin;
public class Cardano extends Coin {
public Cardano() {
super("Cardano", "ADA", new CardanoAddressValidator());
}
}

View file

@ -0,0 +1,36 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.Base58AddressValidator;
import haveno.asset.Coin;
import haveno.asset.NetworkParametersAdapter;
public class Dogecoin extends Coin {
public Dogecoin() {
super("Dogecoin", "DOGE", new Base58AddressValidator(new DogecoinMainNetParams()), Network.MAINNET);
}
public static class DogecoinMainNetParams extends NetworkParametersAdapter {
public DogecoinMainNetParams() {
this.addressHeader = 30;
this.p2shHeader = 22;
}
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.RippleAddressValidator;
public class Ripple extends Coin {
public Ripple() {
super("Ripple", "XRP", new RippleAddressValidator());
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.SolanaAddressValidator;
public class Solana extends Coin {
public Solana() {
super("Solana", "SOL", new SolanaAddressValidator());
}
}

View file

@ -0,0 +1,28 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.Coin;
import haveno.asset.TronAddressValidator;
public class Tron extends Coin {
public Tron() {
super("Tron", "TRX", new TronAddressValidator());
}
}

View file

@ -6,6 +6,6 @@ public class TetherUSDERC20 extends Erc20Token {
public TetherUSDERC20() { public TetherUSDERC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here: // If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (ERC20)", "USDT-ERC20"); super("Tether USD", "USDT-ERC20");
} }
} }

View file

@ -6,6 +6,6 @@ public class TetherUSDTRC20 extends Trc20Token {
public TetherUSDTRC20() { public TetherUSDTRC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here: // If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (TRC20)", "USDT-TRC20"); super("Tether USD", "USDT-TRC20");
} }
} }

View file

@ -22,6 +22,6 @@ import haveno.asset.Erc20Token;
public class USDCoinERC20 extends Erc20Token { public class USDCoinERC20 extends Erc20Token {
public USDCoinERC20() { public USDCoinERC20() {
super("USD Coin (ERC20)", "USDC-ERC20"); super("USD Coin", "USDC-ERC20");
} }
} }

View file

@ -4,9 +4,14 @@
# See https://haveno.exchange/list-asset for complete instructions. # See https://haveno.exchange/list-asset for complete instructions.
haveno.asset.coins.Bitcoin$Mainnet haveno.asset.coins.Bitcoin$Mainnet
haveno.asset.coins.BitcoinCash haveno.asset.coins.BitcoinCash
haveno.asset.coins.Cardano
haveno.asset.coins.Dogecoin
haveno.asset.coins.Ether haveno.asset.coins.Ether
haveno.asset.coins.Litecoin haveno.asset.coins.Litecoin
haveno.asset.coins.Monero haveno.asset.coins.Monero
haveno.asset.coins.Ripple
haveno.asset.coins.Solana
haveno.asset.coins.Tron
haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20 haveno.asset.tokens.TetherUSDTRC20
haveno.asset.tokens.USDCoinERC20 haveno.asset.tokens.USDCoinERC20

View file

@ -4,11 +4,6 @@
# E.g.: [main-view].[component].[description] # E.g.: [main-view].[component].[description]
# In some cases we use enum values or constants to map to display strings # In some cases we use enum values or constants to map to display strings
# A annoying issue with property files is that we need to use 2 single quotes in display string
# containing variables (e.g. {0}), otherwise the variable will not be resolved.
# In display string which do not use a variable a single quote is ok.
# E.g. Don''t .... {1}
# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces
# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose!
# To make longer strings with better readable you can make a line break with \ which does not result in a line break # To make longer strings with better readable you can make a line break with \ which does not result in a line break

View file

@ -0,0 +1,42 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class CardanoTest extends AbstractAssetTest {
public CardanoTest() {
super(new Cardano());
}
@Test
public void testValidAddresses() {
assertValidAddress("addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w");
assertValidAddress("addr1q8gg2r3vf9zggn48g7m8vx62rwf6warcs4k7ej8mdzmqmesj30jz7psduyk6n4n2qrud2xlv9fgj53n6ds3t8cs4fvzs05yzmz");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("addr1Q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu");
assertInvalidAddress("addr2q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu");
assertInvalidAddress("addr2vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w");
assertInvalidAddress("Ae2tdPwUPEYxkYw5GrFyqb4Z9TzXo8f1WnWpPZP1sXrEn1pz2VU3CkJ8aTQ");
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class DogecoinTest extends AbstractAssetTest {
public DogecoinTest() {
super(new Dogecoin());
}
@Test
public void testValidAddresses() {
assertValidAddress("DEa7damK8MsbdCJztidBasZKVsDLJifWfE");
assertValidAddress("DNkkfdUvkCDiywYE98MTVp9nQJTgeZAiFr");
assertValidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("1DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg");
assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxgs");
assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg#");
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class RippleTest extends AbstractAssetTest {
public RippleTest() {
super(new Ripple());
}
@Test
public void testValidAddresses() {
assertValidAddress("r9CxAMAoZAgyVGP8CY9F1arzf9bJg3Y7U8");
assertValidAddress("rsXMbDtCAmzSWajWiii7ffWygAjYVNDxY7");
assertValidAddress("rE3nYkQy121JEVb37JKX8LSH6wUBnNvNo2");
assertValidAddress("rMzucuWFUEE6aM9DC992BqqMgZNPrv4kvi");
assertValidAddress("rJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("RJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
assertInvalidAddress("zJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8");
assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW");
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class SolanaTest extends AbstractAssetTest {
public SolanaTest() {
super(new Solana());
}
@Test
public void testValidAddresses() {
assertValidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33k");
assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz7");
assertValidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpsk");
assertValidAddress("7zVhJcA5s8zfg3UoDUuG4zmnqaVmLqj6L6F6L8WPLnYw");
assertValidAddress("AVHUu155WoNexeNCGce8mrb8hvg8pBgvCJh4vtd3Q1RV");
assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33O");
assertInvalidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpskAAA");
assertInvalidAddress("1");
assertInvalidAddress("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789");
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import org.junit.jupiter.api.Test;
public class TronTest extends AbstractAssetTest {
public TronTest() {
super(new Tron());
}
@Test
public void testValidAddresses() {
assertValidAddress("TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz");
assertValidAddress("THdUXD3mZqT5aMnPQMtBSJX9ANGjaeUwQK");
assertValidAddress("THUE6WTLaEGytFyuGJQUcKc3r245UKypoi");
assertValidAddress("TH7vVF9RTMXM9x7ZnPnbNcEph734hpu8cf");
assertValidAddress("TJNtFduS4oebw3jgGKCYmgSpTdyPieb6Ha");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R");
assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9X");
assertInvalidAddress("1JRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R");
assertInvalidAddress("TGzz8gjYiYRqpfmDwnLxfgPuLVNmpCswVo");
}
}

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.36' moneroJavaVersion = '0.8.38'
httpclient5Version = '5.0' httpclient5Version = '5.0'
hamcrestVersion = '2.2' hamcrestVersion = '2.2'
httpclientVersion = '4.5.12' httpclientVersion = '4.5.12'
@ -79,7 +79,9 @@ configure(subprojects) {
slf4jVersion = '1.7.30' slf4jVersion = '1.7.30'
sparkVersion = '2.5.2' sparkVersion = '2.5.2'
os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os def osName = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
def osArch = System.getProperty("os.arch").toLowerCase()
os = (osName == 'mac' && (osArch.contains('aarch64') || osArch.contains('arm'))) ? 'mac-aarch64' : osName
} }
repositories { repositories {
@ -457,14 +459,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/release6/monero-bins-haveno-linux-x86_64.tar.gz', 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208', 'linux-x86_64-sha256' : '713d64ff6423add0d065d9dfbf8a120dfbf3995d4b2093f8235b4da263d8a89c',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz', 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c', 'linux-aarch64-sha256' : '332dcc6a5d7eec754c010a1f893f81656be1331b847b06e9be69293b456f67cc',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz', 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8', 'mac-sha256' : '1c5bcd23373132528634352e604c1d732a73c634f3c77314fae503c6d23e10b0',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip', 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-windows.zip',
'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d' 'windows-sha256' : '3d57b980e0208a950fd795f442d9e087b5298a914b0bd96fec431188b5ab0dad'
] ]
String osKey String osKey
@ -610,7 +612,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.19-SNAPSHOT' version = '1.2.1-SNAPSHOT'
jar.manifest.attributes( jar.manifest.attributes(
"Implementation-Title": project.name, "Implementation-Title": project.name,

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.19"; public static final String VERSION = "1.2.1";
/** /**
* 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.
@ -107,12 +107,11 @@ public class Version {
// The version no. of the current protocol. The offer holds that version. // The version no. of the current protocol. The offer holds that version.
// A taker will check the version of the offers to see if his version is compatible. // A taker will check the version of the offers to see if his version is compatible.
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app. // the Haveno app.
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2; // Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3
public static final int TRADE_PROTOCOL_VERSION = 3;
private static String p2pMessageVersion; private static String p2pMessageVersion;
public static String getP2PMessageVersion() { public static String getP2PMessageVersion() {

View file

@ -119,6 +119,7 @@ public class Config {
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 UPDATE_XMR_BINARIES = "updateXmrBinaries";
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
public static final String DISABLE_RATE_LIMITS = "disableRateLimits";
// Default values for certain options // Default values for certain options
public static final int UNSPECIFIED_PORT = -1; public static final int UNSPECIFIED_PORT = -1;
@ -208,6 +209,7 @@ public class Config {
public final boolean passwordRequired; public final boolean passwordRequired;
public final boolean updateXmrBinaries; public final boolean updateXmrBinaries;
public final String xmrBlockchainPath; public final String xmrBlockchainPath;
public final boolean disableRateLimits;
// 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;
@ -639,6 +641,13 @@ public class Config {
.ofType(String.class) .ofType(String.class)
.defaultsTo(""); .defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> disableRateLimits =
parser.accepts(DISABLE_RATE_LIMITS,
"Disables all API rate limits")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
try { try {
CompositeOptionSet options = new CompositeOptionSet(); CompositeOptionSet options = new CompositeOptionSet();
@ -753,6 +762,7 @@ public class Config {
this.passwordRequired = options.valueOf(passwordRequiredOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt);
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
this.disableRateLimits = options.valueOf(disableRateLimits);
} 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),

View file

@ -433,12 +433,14 @@ public class PersistenceManager<T extends PersistableEnvelope> {
private void maybeStartTimerForPersistence() { private void maybeStartTimerForPersistence() {
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays // We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
// can be rather long. // can be rather long.
UserThread.execute(() -> {
if (timer == null) { if (timer == null) {
timer = UserThread.runAfter(() -> { timer = UserThread.runAfter(() -> {
persistNow(null); persistNow(null);
UserThread.execute(() -> timer = null); UserThread.execute(() -> timer = null);
}, source.delay, TimeUnit.MILLISECONDS); }, source.delay, TimeUnit.MILLISECONDS);
} }
});
} }
public void forcePersistNow() { public void forcePersistNow() {

View file

@ -22,6 +22,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque; import java.util.Deque;
@ -85,6 +86,11 @@ public class MathUtils {
return ((double) value) * factor; return ((double) value) * factor;
} }
public static BigInteger scaleUpByPowerOf10(BigInteger value, int exponent) {
BigInteger factor = BigInteger.TEN.pow(exponent);
return value.multiply(factor);
}
public static double scaleDownByPowerOf10(double value, int exponent) { public static double scaleDownByPowerOf10(double value, int exponent) {
double factor = Math.pow(10, exponent); double factor = Math.pow(10, exponent);
return value / factor; return value / factor;
@ -95,6 +101,11 @@ public class MathUtils {
return ((double) value) / factor; return ((double) value) / factor;
} }
public static BigInteger scaleDownByPowerOf10(BigInteger value, int exponent) {
BigInteger factor = BigInteger.TEN.pow(exponent);
return value.divide(factor);
}
public static double exactMultiply(double value1, double value2) { public static double exactMultiply(double value1, double value2) {
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
} }

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

@ -654,7 +654,7 @@ public class AccountAgeWitnessService {
Date peersCurrentDate, Date peersCurrentDate,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer); checkNotNull(offer);
final String currencyCode = offer.getCurrencyCode(); final String currencyCode = offer.getCounterCurrencyCode();
final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode); final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode);
BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit; BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit;
if (!hasTradeLimitException(peersWitness)) { if (!hasTradeLimitException(peersWitness)) {
@ -673,7 +673,7 @@ public class AccountAgeWitnessService {
"\nPeers trade limit=" + peersCurrentTradeLimit + "\nPeers trade limit=" + peersCurrentTradeLimit +
"\nOffer ID=" + offer.getShortId() + "\nOffer ID=" + offer.getShortId() +
"\nPaymentMethod=" + offer.getPaymentMethod().getId() + "\nPaymentMethod=" + offer.getPaymentMethod().getId() +
"\nCurrencyCode=" + offer.getCurrencyCode(); "\nCurrencyCode=" + offer.getCounterCurrencyCode();
log.warn(msg); log.warn(msg);
errorMessageHandler.handleErrorMessage(msg); errorMessageHandler.handleErrorMessage(msg);
} }

View file

@ -140,7 +140,7 @@ public class AccountAgeWitnessUtils {
boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) &&
!accountAgeWitnessService.peerHasSignedWitness(trade) && !accountAgeWitnessService.peerHasSignedWitness(trade) &&
accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()); accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount());
log.info("AccountSigning debug log: " + log.debug("AccountSigning debug log: " +
"\ntradeId: {}" + "\ntradeId: {}" +
"\nis buyer: {}" + "\nis buyer: {}" +
"\nbuyer account age witness info: {}" + "\nbuyer account age witness info: {}" +

View file

@ -105,9 +105,9 @@ public class AlertManager {
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
case XMR_STAGENET: case XMR_STAGENET:
return List.of( return List.of(
"036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f", "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36"); "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
case XMR_MAINNET: case XMR_MAINNET:
return List.of(); return List.of();
default: default:

View file

@ -104,9 +104,9 @@ public class PrivateNotificationManager implements MessageListener {
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
case XMR_STAGENET: case XMR_STAGENET:
return List.of( return List.of(
"02ba7c5de295adfe57b60029f3637a2c6b1d0e969a8aaefb9e0ddc3a7963f26925", "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854", "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36"); "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
case XMR_MAINNET: case XMR_MAINNET:
return List.of(); return List.of();
default: default:

View file

@ -299,8 +299,12 @@ public class CoreApi {
return walletsService.createXmrTx(destinations); return walletsService.createXmrTx(destinations);
} }
public String relayXmrTx(String metadata) { public List<MoneroTxWallet> createXmrSweepTxs(String address) {
return walletsService.relayXmrTx(metadata); return walletsService.createXmrSweepTxs(address);
}
public List<String> relayXmrTxs(List<String> metadatas) {
return walletsService.relayXmrTxs(metadatas);
} }
public long getAddressBalance(String addressString) { public long getAddressBalance(String addressString) {

View file

@ -241,12 +241,24 @@ public class CoreDisputesService {
} else if (payoutSuggestion == PayoutSuggestion.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);
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
log.warn("Payout amount for buyer is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()),
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
disputeResult.setBuyerPayoutAmountBeforeCost(trade.getWallet().getBalance());
}
} else if (payoutSuggestion == PayoutSuggestion.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 (payoutSuggestion == PayoutSuggestion.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));
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
log.warn("Payout amount for seller is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()),
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
disputeResult.setSellerPayoutAmountBeforeCost(trade.getWallet().getBalance());
}
} else if (payoutSuggestion == PayoutSuggestion.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();

View file

@ -149,7 +149,7 @@ public class CoreOffersService {
List<OpenOffer> getMyOffers(String direction, String currencyCode) { List<OpenOffer> getMyOffers(String direction, String currencyCode) {
return getMyOffers().stream() return getMyOffers().stream()
.filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode)) .filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
.sorted(openOfferPriceComparator(direction, CurrencyUtil.isTraditionalCurrency(currencyCode))) .sorted(openOfferPriceComparator(direction))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -336,7 +336,7 @@ public class CoreOffersService {
String sourceOfferId, 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.getCounterCurrencyCode());
openOfferManager.placeOffer(offer, openOfferManager.placeOffer(offer,
useSavingsWallet, useSavingsWallet,
triggerPriceAsLong, triggerPriceAsLong,
@ -353,8 +353,7 @@ public class CoreOffersService {
if ("".equals(direction)) direction = null; if ("".equals(direction)) direction = null;
if ("".equals(currencyCode)) currencyCode = null; if ("".equals(currencyCode)) currencyCode = null;
var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction); var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction);
var counterAssetCode = CurrencyUtil.isCryptoCurrency(currencyCode) ? offer.getOfferPayload().getBaseCurrencyCode() : offer.getOfferPayload().getCounterCurrencyCode(); var offerInWantedCurrency = currencyCode == null || offer.getCounterCurrencyCode().equalsIgnoreCase(currencyCode);
var offerInWantedCurrency = currencyCode == null || counterAssetCode.equalsIgnoreCase(currencyCode);
return offerOfWantedDirection && offerInWantedCurrency; return offerOfWantedDirection && offerInWantedCurrency;
} }
@ -366,17 +365,12 @@ public class CoreOffersService {
: priceComparator.get(); : priceComparator.get();
} }
private Comparator<OpenOffer> openOfferPriceComparator(String direction, boolean isTraditional) { private Comparator<OpenOffer> openOfferPriceComparator(String direction) {
// A buyer probably wants to see sell orders in price ascending order. // A buyer probably wants to see sell orders in price ascending order.
// A seller probably wants to see buy orders in price descending order. // A seller probably wants to see buy orders in price descending order.
if (isTraditional)
return direction.equalsIgnoreCase(OfferDirection.BUY.name()) return direction.equalsIgnoreCase(OfferDirection.BUY.name())
? openOfferPriceComparator.get().reversed() ? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get(); : openOfferPriceComparator.get();
else
return direction.equalsIgnoreCase(OfferDirection.SELL.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
} }
private long priceStringToLong(String priceAsString, String currencyCode) { private long priceStringToLong(String priceAsString, String currencyCode) {

View file

@ -36,6 +36,8 @@ import haveno.core.payment.InstantCryptoCurrencyAccount;
import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccount;
import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.PaymentAccountFactory;
import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PaymentMethod;
import haveno.core.payment.validation.InteracETransferValidator;
import haveno.core.trade.HavenoUtils;
import haveno.core.user.User; import haveno.core.user.User;
import java.io.File; import java.io.File;
import static java.lang.String.format; import static java.lang.String.format;
@ -48,19 +50,24 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
@Slf4j @Slf4j
class CorePaymentAccountsService { public class CorePaymentAccountsService {
private final CoreAccountService accountService; private final CoreAccountService accountService;
private final AccountAgeWitnessService accountAgeWitnessService; private final AccountAgeWitnessService accountAgeWitnessService;
private final User user; private final User user;
public final InteracETransferValidator interacETransferValidator;
@Inject @Inject
public CorePaymentAccountsService(CoreAccountService accountService, public CorePaymentAccountsService(CoreAccountService accountService,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
User user) { User user,
InteracETransferValidator interacETransferValidator) {
this.accountService = accountService; this.accountService = accountService;
this.accountAgeWitnessService = accountAgeWitnessService; this.accountAgeWitnessService = accountAgeWitnessService;
this.user = user; this.user = user;
this.interacETransferValidator = interacETransferValidator;
HavenoUtils.corePaymentAccountService = this;
} }
PaymentAccount createPaymentAccount(PaymentAccountForm form) { PaymentAccount createPaymentAccount(PaymentAccountForm form) {

View file

@ -74,9 +74,11 @@ class CorePriceService {
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
if (marketPrice == null) { if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined?
} else if (!marketPrice.isExternallyProvidedPrice()) {
throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean
} }
return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); return marketPrice.getPrice();
} }
/** /**
@ -85,8 +87,7 @@ class CorePriceService {
public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException { public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
return priceFeedService.requestAllPrices().values().stream() return priceFeedService.requestAllPrices().values().stream()
.map(marketPrice -> { .map(marketPrice -> {
double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); return new MarketPriceInfo(marketPrice.getCurrencyCode(), marketPrice.getPrice());
return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice);
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -100,12 +101,13 @@ class CorePriceService {
// Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used.
Comparator<Offer> offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); Comparator<Offer> offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder()));
// TODO: remove this!!!
// Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is // Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is
// considered as buying/selling crypto. Because of this, when viewing a xmr-crypto pair, // considered as buying/selling crypto. Because of this, when viewing a xmr-crypto pair,
// the buy column is actually the sell column and vice versa. To maintain the expected // the buy column is actually the sell column and vice versa. To maintain the expected
// ordering, we have to reverse the price comparator. // ordering, we have to reverse the price comparator.
boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode); //boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode);
if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); //if (isCrypto) offerPriceComparator = offerPriceComparator.reversed();
// Offer amounts are used for the secondary sort. They are sorted from high to low. // Offer amounts are used for the secondary sort. They are sorted from high to low.
Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed();
@ -128,11 +130,11 @@ class CorePriceService {
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
accumulatedAmount += amount; accumulatedAmount += amount;
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
buyTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); buyTM.put(priceAsDouble, accumulatedAmount);
} }
}; };
// Create buyer hashmap {key:price, value:count}, uses TreeMap to sort by key (asc) // Create seller hashmap {key:price, value:count}, uses TreeMap to sort by key (asc)
accumulatedAmount = 0; accumulatedAmount = 0;
LinkedHashMap<Double,Double> sellTM = new LinkedHashMap<Double,Double>(); LinkedHashMap<Double,Double> sellTM = new LinkedHashMap<Double,Double>();
for(Offer offer: sellOffers){ for(Offer offer: sellOffers){
@ -141,7 +143,7 @@ class CorePriceService {
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
accumulatedAmount += amount; accumulatedAmount += amount;
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
sellTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); sellTM.put(priceAsDouble, accumulatedAmount);
} }
}; };
@ -155,20 +157,5 @@ class CorePriceService {
return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth); return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth);
} }
/**
* PriceProvider returns different values for crypto and traditional,
* e.g. 1 XMR = X USD
* but 1 DOGE = X XMR
* Here we convert all to:
* 1 XMR = X (FIAT or CRYPTO)
*/
private double mapPriceFeedServicePrice(double price, String currencyCode) {
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
return price;
}
return price == 0 ? 0 : 1 / price;
// TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore
}
} }

View file

@ -123,17 +123,14 @@ class CoreTradesService {
BigInteger amount = amountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(amountAsLong); BigInteger amount = amountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(amountAsLong);
// 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.getCounterCurrencyCode();
OfferDirection direction = offer.getOfferPayload().getDirection(); OfferDirection direction = offer.getOfferPayload().getDirection();
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); BigInteger maxAmount = 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(), offer.getMinAmount(), maxAmount);
} else if (offer.isTraditionalOffer() } else if (offer.isTraditionalOffer() && offer.isRange()) {
&& !amount.equals(offer.getMinAmount()) && !amount.equals(amount)) { amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId());
// We only apply the rounding if the amount is variable (minAmount is lower as amount).
// Otherwise we could get an amount lower then the minAmount set by rounding
amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), maxTradeLimit, offer.getCurrencyCode(), offer.getPaymentMethodId());
} }
} }
@ -192,7 +189,6 @@ class CoreTradesService {
verifyTradeIsNotClosed(tradeId); verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() -> var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
log.info("Keeping funds received from trade {}", tradeId);
tradeManager.onTradeCompleted(trade); tradeManager.onTradeCompleted(trade);
} }

View file

@ -173,12 +173,24 @@ class CoreWalletsService {
} }
} }
String relayXmrTx(String metadata) { List<MoneroTxWallet> createXmrSweepTxs(String address) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked(); verifyEncryptedWalletIsUnlocked();
try { try {
return xmrWalletService.relayTx(metadata); return xmrWalletService.createSweepTxs(address);
} catch (Exception ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
}
List<String> relayXmrTxs(List<String> metadatas) {
accountService.checkAccountOpen();
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
try {
return xmrWalletService.relayTxs(metadatas);
} catch (Exception ex) { } catch (Exception ex) {
log.error("", ex); log.error("", ex);
throw new IllegalStateException(ex); throw new IllegalStateException(ex);

View file

@ -24,6 +24,7 @@ import haveno.common.UserThread;
import haveno.common.app.DevEnv; import haveno.common.app.DevEnv;
import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.BaseCurrencyNetwork;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.model.EncryptedConnectionList;
@ -74,10 +75,13 @@ public final class XmrConnectionService {
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_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
private static final int MAX_CONSECUTIVE_ERRORS = 3; // max errors before switching connections
private static int numConsecutiveErrors = 0;
public enum XmrConnectionError { public enum XmrConnectionFallbackType {
LOCAL, LOCAL,
CUSTOM CUSTOM,
PROVIDED
} }
private final Object lock = new Object(); private final Object lock = new Object();
@ -92,12 +96,12 @@ public final class XmrConnectionService {
private final MoneroConnectionManager connectionManager; private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList; private final EncryptedConnectionList connectionList;
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>(); private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
private final IntegerProperty numConnections = new SimpleIntegerProperty(0); private final IntegerProperty numConnections = new SimpleIntegerProperty(-1);
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>(); private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
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<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>(); private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter @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);
@ -105,15 +109,15 @@ public final class XmrConnectionService {
private boolean isInitialized; private boolean isInitialized;
private boolean pollInProgress; private boolean pollInProgress;
private MoneroDaemonRpc daemon; private MoneroDaemonRpc monerod;
private Boolean isConnected = false; private Boolean isConnected = false;
@Getter @Getter
private MoneroDaemonInfo lastInfo; private MoneroDaemonInfo lastInfo;
private Long lastFallbackInvocation; private Long lastFallbackInvocation;
private Long lastLogPollErrorTimestamp; private Long lastLogPollErrorTimestamp;
private long lastLogDaemonNotSyncedTimestamp; private long lastLogMonerodNotSyncedTimestamp;
private Long syncStartHeight; private Long syncStartHeight;
private TaskLooper daemonPollLooper; private TaskLooper monerodPollLooper;
private long lastRefreshPeriodMs; private long lastRefreshPeriodMs;
@Getter @Getter
private boolean isShutDownStarted; private boolean isShutDownStarted;
@ -129,6 +133,7 @@ public final class XmrConnectionService {
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 static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
private boolean fallbackApplied; private boolean fallbackApplied;
private boolean usedSyncingLocalNodeBeforeStartup;
@Inject @Inject
public XmrConnectionService(P2PService p2PService, public XmrConnectionService(P2PService p2PService,
@ -156,7 +161,13 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() { p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
ThreadUtils.submitToPool(() -> initialize()); ThreadUtils.submitToPool(() -> {
try {
initialize();
} catch (Exception e) {
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
}
});
} }
@Override @Override
public void onHiddenServicePublished() {} public void onHiddenServicePublished() {}
@ -180,16 +191,16 @@ public final class XmrConnectionService {
log.info("Shutting down {}", getClass().getSimpleName()); log.info("Shutting down {}", getClass().getSimpleName());
isInitialized = false; isInitialized = false;
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (monerodPollLooper != null) monerodPollLooper.stop();
daemon = null; monerod = null;
} }
} }
// ------------------------ CONNECTION MANAGEMENT ------------------------- // ------------------------ CONNECTION MANAGEMENT -------------------------
public MoneroDaemonRpc getDaemon() { public MoneroDaemonRpc getMonerod() {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
return this.daemon; return this.monerod;
} }
public String getProxyUri() { public String getProxyUri() {
@ -270,7 +281,7 @@ public final class XmrConnectionService {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
// user needs to authorize fallback on startup after using locally synced node // user needs to authorize fallback on startup after using locally synced node
if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { if (fallbackRequiredBeforeConnectionSwitch()) {
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
return null; return null;
} }
@ -283,6 +294,10 @@ public final class XmrConnectionService {
return bestConnection; 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) {
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
} }
@ -390,7 +405,7 @@ public final class XmrConnectionService {
} }
public void verifyConnection() { public void verifyConnection() {
if (daemon == null) throw new RuntimeException("No connection to Monero node"); if (monerod == null) throw new RuntimeException("No connection to Monero node");
if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node"); if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node");
if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced"); if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced");
} }
@ -433,6 +448,7 @@ public final class XmrConnectionService {
} }
public boolean hasSufficientPeersForBroadcast() { public boolean hasSufficientPeersForBroadcast() {
if (numConnections.get() < 0) return true; // we don't know how many connections we have, but that's expected with restricted node
return numConnections.get() >= getMinBroadcastConnections(); return numConnections.get() >= getMinBroadcastConnections();
} }
@ -458,15 +474,20 @@ public final class XmrConnectionService {
public void fallbackToBestConnection() { public void fallbackToBestConnection() {
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (xmrNodes.getProvidedXmrNodes().isEmpty()) { fallbackApplied = true;
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
log.warn("Falling back to public nodes"); log.warn("Falling back to public nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
initializeConnections();
} else { } else {
log.warn("Falling back to provided nodes"); log.warn("Falling back to provided nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
}
fallbackApplied = true;
initializeConnections(); initializeConnections();
if (getConnection() == null) {
log.warn("No provided nodes available, falling back to public nodes");
fallbackToBestConnection();
}
}
} }
// ------------------------------- HELPERS -------------------------------- // ------------------------------- HELPERS --------------------------------
@ -548,8 +569,8 @@ public final class XmrConnectionService {
// register local node listener // register local node listener
xmrLocalNode.addListener(new XmrLocalNodeListener() { xmrLocalNode.addListener(new XmrLocalNodeListener() {
@Override @Override
public void onNodeStarted(MoneroDaemonRpc daemon) { public void onNodeStarted(MoneroDaemonRpc monerod) {
log.info("Local monero node started, height={}", daemon.getHeight()); log.info("Local monero node started, height={}", monerod.getHeight());
} }
@Override @Override
@ -578,8 +599,8 @@ public final class XmrConnectionService {
setConnection(connection.getUri()); setConnection(connection.getUri());
// reset error connecting to local node // reset error connecting to local node
if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
connectionServiceError.set(null); connectionServiceFallbackType.set(null);
} }
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestConnection(); MoneroRpcConnection bestConnection = getBestConnection();
@ -602,9 +623,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);
@ -615,9 +638,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);
@ -632,6 +657,11 @@ public final class XmrConnectionService {
} }
} }
// 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()) {
@ -666,43 +696,30 @@ public final class XmrConnectionService {
onConnectionChanged(connectionManager.getConnection()); onConnectionChanged(connectionManager.getConnection());
} }
private boolean lastUsedLocalSyncingNode() { public void startLocalNode() throws Exception {
return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored();
}
public void startLocalNode() {
// cannot start local node as seed node // cannot start local node as seed node
if (HavenoUtils.isSeedNode()) { if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node"); 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);
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Local node is not offline and used as last connection");
}
} }
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", new Throwable("Stack trace")); log.warn("Setting monerod connection to null", new Throwable("Stack trace"));
} }
synchronized (lock) { synchronized (lock) {
if (currentConnection == null) { if (currentConnection == null) {
daemon = null; monerod = null;
isConnected = false; isConnected = false;
connectionList.setCurrentConnectionUri(null); connectionList.setCurrentConnectionUri(null);
} else { } else {
daemon = new MoneroDaemonRpc(currentConnection); monerod = new MoneroDaemonRpc(currentConnection);
isConnected = currentConnection.isConnected(); isConnected = currentConnection.isConnected();
connectionList.removeConnection(currentConnection.getUri()); connectionList.removeConnection(currentConnection.getUri());
connectionList.addConnection(currentConnection); connectionList.addConnection(currentConnection);
@ -717,11 +734,11 @@ public final class XmrConnectionService {
} }
// update key image poller // update key image poller
keyImagePoller.setDaemon(getDaemon()); keyImagePoller.setMonerod(getMonerod());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling // update polling
doPollDaemon(); doPollMonerod();
if (currentConnection != getConnection()) return; // polling can change connection if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000); UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
@ -741,74 +758,87 @@ public final class XmrConnectionService {
private void startPolling() { private void startPolling() {
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (monerodPollLooper != null) monerodPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemon()); monerodPollLooper = new TaskLooper(() -> pollMonerod());
daemonPollLooper.start(getInternalRefreshPeriodMs()); monerodPollLooper.start(getInternalRefreshPeriodMs());
} }
} }
private void stopPolling() { private void stopPolling() {
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) { if (monerodPollLooper != null) {
daemonPollLooper.stop(); monerodPollLooper.stop();
daemonPollLooper = null; monerodPollLooper = null;
} }
} }
} }
private void pollDaemon() { private void pollMonerod() {
if (pollInProgress) return; if (pollInProgress) return;
doPollDaemon(); doPollMonerod();
} }
private void doPollDaemon() { private void doPollMonerod() {
synchronized (pollLock) { synchronized (pollLock) {
pollInProgress = true; pollInProgress = true;
if (isShutDownStarted) return; if (isShutDownStarted) return;
try { try {
// poll daemon // poll monerod
if (daemon == null) switchToBestConnection(); if (monerod == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
try { try {
if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); if (monerod == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo(); lastInfo = monerod.getInfo();
numConsecutiveErrors = 0;
} catch (Exception e) { } catch (Exception e) {
// skip handling if shutting down // skip handling if shutting down
if (isShutDownStarted) return; if (isShutDownStarted) return;
// invoke fallback handling on startup error // skip error handling up to max attempts
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); numConsecutiveErrors++;
if (lastInfo == null && canFallback) { if (numConsecutiveErrors <= MAX_CONSECUTIVE_ERRORS) {
if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { return;
lastFallbackInvocation = System.currentTimeMillis();
if (lastUsedLocalSyncingNode()) {
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
connectionServiceError.set(XmrConnectionError.LOCAL);
} else { } else {
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); numConsecutiveErrors = 0; // reset error count
connectionServiceError.set(XmrConnectionError.CUSTOM); }
// invoke fallback handling on startup error
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (lastInfo == null && canFallback) {
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
lastFallbackInvocation = System.currentTimeMillis();
if (usedSyncingLocalNodeBeforeStartup) {
log.warn("Failed to fetch monerod info from local connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
} else if (isProvidedConnections()) {
log.warn("Failed to fetch monerod info from provided connections on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
} else {
log.warn("Failed to fetch monerod info from custom connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
} }
} }
return; return;
} }
// log error message periodically // log error message periodically
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { if (lastWarningOutsidePeriod()) {
log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage()); MoneroRpcConnection connection = getConnection();
log.warn("Error fetching daemon info after max attempts. Trying to switch to best connection. monerod={}, error={}", connection == null ? "null" : connection.getUri(), e.getMessage());
if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e)); if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e));
lastLogPollErrorTimestamp = System.currentTimeMillis(); lastLogPollErrorTimestamp = System.currentTimeMillis();
} }
// switch to best connection // switch to best connection
switchToBestConnection(); switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling"); if (monerod == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = daemon.getInfo(); // caught internally if still fails lastInfo = monerod.getInfo(); // caught internally if still fails
} }
// connected to daemon // connected to monerod
isConnected = true; isConnected = true;
connectionServiceError.set(null); 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
@ -816,10 +846,10 @@ public final class XmrConnectionService {
// write sync status to preferences // write sync status to preferences
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing); preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
// throttle warnings if daemon not synced // throttle warnings if monerod not synced
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) { if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogMonerodNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) {
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight()); log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight());
lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis(); lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis();
} }
// announce connection change if refresh period changes // announce connection change if refresh period changes
@ -829,6 +859,9 @@ public final class XmrConnectionService {
return; return;
} }
// get the number of connections, which is only available if not restricted
int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections();
// update properties on user thread // update properties on user thread
UserThread.execute(() -> { UserThread.execute(() -> {
@ -854,15 +887,22 @@ public final class XmrConnectionService {
} }
} }
connections.set(availableConnections); connections.set(availableConnections);
numConnections.set(availableConnections.size()); numConnections.set(numOutgoingConnections);
// notify update // notify update
numUpdates.set(numUpdates.get() + 1); numUpdates.set(numUpdates.get() + 1);
}); });
// invoke error handling if no connections
if (numOutgoingConnections == 0) {
String errorMsg = "The Monero node has no connected peers. It may be experiencing a network connectivity issue.";
log.warn(errorMsg);
throw new RuntimeException(errorMsg);
}
// handle error recovery // handle error recovery
if (lastLogPollErrorTimestamp != null) { if (lastLogPollErrorTimestamp != null) {
log.info("Successfully fetched daemon info after previous error"); log.info("Successfully fetched monerod info after previous error");
lastLogPollErrorTimestamp = null; lastLogPollErrorTimestamp = null;
} }
@ -870,25 +910,40 @@ public final class XmrConnectionService {
getConnectionServiceErrorMsg().set(null); getConnectionServiceErrorMsg().set(null);
} catch (Exception e) { } catch (Exception e) {
// not connected to daemon // not connected to monerod
isConnected = false; isConnected = false;
// skip if shut down // skip if shut down
if (isShutDownStarted) return; if (isShutDownStarted) return;
// format error message
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.contains(": ")) {
errorMsg = errorMsg.substring(errorMsg.indexOf(": ") + 2); // strip exception class
}
errorMsg = Res.get("popup.warning.moneroConnection", errorMsg);
// set error message // set error message
getConnectionServiceErrorMsg().set(e.getMessage()); getConnectionServiceErrorMsg().set(errorMsg);
} finally { } finally {
pollInProgress = false; pollInProgress = false;
} }
} }
} }
private boolean lastWarningOutsidePeriod() {
return lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS;
}
private boolean isFixedConnection() { private boolean isFixedConnection() {
return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
} }
private boolean isCustomConnections() { private boolean isCustomConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
} }
private boolean isProvidedConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
}
} }

View file

@ -109,17 +109,18 @@ public class XmrLocalNode {
public boolean shouldBeIgnored() { public boolean shouldBeIgnored() {
if (config.ignoreLocalXmrNode) return true; if (config.ignoreLocalXmrNode) return true;
// determine if local node is configured // 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; boolean hasConfiguredLocalNode = false;
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
hasConfiguredLocalNode = true; hasConfiguredLocalNode = true;
break; break;
} }
} }
if (!hasConfiguredLocalNode) return true; return !hasConfiguredLocalNode;
return false;
} }
public void addListener(XmrLocalNodeListener listener) { public void addListener(XmrLocalNodeListener listener) {

View file

@ -129,7 +129,7 @@ public class OfferInfo implements Payload {
public static OfferInfo toMyOfferInfo(OpenOffer openOffer) { public static OfferInfo toMyOfferInfo(OpenOffer openOffer) {
// An OpenOffer is always my offer. // An OpenOffer is always my offer.
var offer = openOffer.getOffer(); var offer = openOffer.getOffer();
var currencyCode = offer.getCurrencyCode(); var currencyCode = offer.getCounterCurrencyCode();
var isActivated = !openOffer.isDeactivated(); var isActivated = !openOffer.isDeactivated();
Optional<Price> optionalTriggerPrice = openOffer.getTriggerPrice() > 0 Optional<Price> optionalTriggerPrice = openOffer.getTriggerPrice() > 0
? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice())) ? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice()))
@ -150,7 +150,7 @@ public class OfferInfo implements Payload {
private static OfferInfoBuilder getBuilder(Offer offer) { private static OfferInfoBuilder getBuilder(Offer offer) {
// OfferInfo protos are passed to API client, and some field // OfferInfo protos are passed to API client, and some field
// values are converted to displayable, unambiguous form. // values are converted to displayable, unambiguous form.
var currencyCode = offer.getCurrencyCode(); var currencyCode = offer.getCounterCurrencyCode();
var preciseOfferPrice = reformatMarketPrice( var preciseOfferPrice = reformatMarketPrice(
requireNonNull(offer.getPrice()).toPlainString(), requireNonNull(offer.getPrice()).toPlainString(),
currencyCode); currencyCode);

View file

@ -78,7 +78,15 @@ public final class PaymentAccountForm implements PersistablePayload {
CASH_APP, CASH_APP,
PAYPAL, PAYPAL,
VENMO, VENMO,
PAYSAFE; PAYSAFE,
WECHAT_PAY,
ALI_PAY,
SWISH,
TRANSFERWISE_USD,
AMAZON_GIFT_CARD,
ACH_TRANSFER,
INTERAC_E_TRANSFER,
US_POSTAL_MONEY_ORDER;
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

@ -57,7 +57,7 @@ public class TradeInfo implements Payload {
private static final Function<Trade, String> toPreciseTradePrice = (trade) -> private static final Function<Trade, String> toPreciseTradePrice = (trade) ->
reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(), reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(),
trade.getOffer().getCurrencyCode()); trade.getOffer().getCounterCurrencyCode());
// Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol). // Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol).
private final OfferInfo offer; private final OfferInfo offer;
@ -91,14 +91,19 @@ public class TradeInfo implements Payload {
private final boolean isDepositsPublished; private final boolean isDepositsPublished;
private final boolean isDepositsConfirmed; private final boolean isDepositsConfirmed;
private final boolean isDepositsUnlocked; private final boolean isDepositsUnlocked;
private final boolean isDepositsFinalized;
private final boolean isPaymentSent; private final boolean isPaymentSent;
private final boolean isPaymentReceived; private final boolean isPaymentReceived;
private final boolean isPayoutPublished; private final boolean isPayoutPublished;
private final boolean isPayoutConfirmed; private final boolean isPayoutConfirmed;
private final boolean isPayoutUnlocked; private final boolean isPayoutUnlocked;
private final boolean isPayoutFinalized;
private final boolean isCompleted; private final boolean isCompleted;
private final String contractAsJson; private final String contractAsJson;
private final ContractInfo contract; private final ContractInfo contract;
private final long startTime;
private final long maxDurationMs;
private final long deadlineTime;
public TradeInfo(TradeInfoV1Builder builder) { public TradeInfo(TradeInfoV1Builder builder) {
this.offer = builder.getOffer(); this.offer = builder.getOffer();
@ -132,14 +137,19 @@ public class TradeInfo implements Payload {
this.isDepositsPublished = builder.isDepositsPublished(); this.isDepositsPublished = builder.isDepositsPublished();
this.isDepositsConfirmed = builder.isDepositsConfirmed(); this.isDepositsConfirmed = builder.isDepositsConfirmed();
this.isDepositsUnlocked = builder.isDepositsUnlocked(); this.isDepositsUnlocked = builder.isDepositsUnlocked();
this.isDepositsFinalized = builder.isDepositsFinalized();
this.isPaymentSent = builder.isPaymentSent(); this.isPaymentSent = builder.isPaymentSent();
this.isPaymentReceived = builder.isPaymentReceived(); this.isPaymentReceived = builder.isPaymentReceived();
this.isPayoutPublished = builder.isPayoutPublished(); this.isPayoutPublished = builder.isPayoutPublished();
this.isPayoutConfirmed = builder.isPayoutConfirmed(); this.isPayoutConfirmed = builder.isPayoutConfirmed();
this.isPayoutUnlocked = builder.isPayoutUnlocked(); this.isPayoutUnlocked = builder.isPayoutUnlocked();
this.isPayoutFinalized = builder.isPayoutFinalized();
this.isCompleted = builder.isCompleted(); this.isCompleted = builder.isCompleted();
this.contractAsJson = builder.getContractAsJson(); this.contractAsJson = builder.getContractAsJson();
this.contract = builder.getContract(); this.contract = builder.getContract();
this.startTime = builder.getStartTime();
this.maxDurationMs = builder.getMaxDurationMs();
this.deadlineTime = builder.getDeadlineTime();
} }
public static TradeInfo toTradeInfo(Trade trade) { public static TradeInfo toTradeInfo(Trade trade) {
@ -193,15 +203,20 @@ public class TradeInfo implements Payload {
.withIsDepositsPublished(trade.isDepositsPublished()) .withIsDepositsPublished(trade.isDepositsPublished())
.withIsDepositsConfirmed(trade.isDepositsConfirmed()) .withIsDepositsConfirmed(trade.isDepositsConfirmed())
.withIsDepositsUnlocked(trade.isDepositsUnlocked()) .withIsDepositsUnlocked(trade.isDepositsUnlocked())
.withIsDepositsFinalized(trade.isDepositsFinalized())
.withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentSent(trade.isPaymentSent())
.withIsPaymentReceived(trade.isPaymentReceived()) .withIsPaymentReceived(trade.isPaymentReceived())
.withIsPayoutPublished(trade.isPayoutPublished()) .withIsPayoutPublished(trade.isPayoutPublished())
.withIsPayoutConfirmed(trade.isPayoutConfirmed()) .withIsPayoutConfirmed(trade.isPayoutConfirmed())
.withIsPayoutUnlocked(trade.isPayoutUnlocked()) .withIsPayoutUnlocked(trade.isPayoutUnlocked())
.withIsPayoutFinalized(trade.isPayoutFinalized())
.withIsCompleted(trade.isCompleted()) .withIsCompleted(trade.isCompleted())
.withContractAsJson(trade.getContractAsJson()) .withContractAsJson(trade.getContractAsJson())
.withContract(contractInfo) .withContract(contractInfo)
.withOffer(toOfferInfo(trade.getOffer())) .withOffer(toOfferInfo(trade.getOffer()))
.withStartTime(trade.getStartDate().getTime())
.withMaxDurationMs(trade.getMaxTradePeriod())
.withDeadlineTime(trade.getMaxTradePeriodDate().getTime())
.build(); .build();
} }
@ -243,14 +258,19 @@ public class TradeInfo implements Payload {
.setIsDepositsPublished(isDepositsPublished) .setIsDepositsPublished(isDepositsPublished)
.setIsDepositsConfirmed(isDepositsConfirmed) .setIsDepositsConfirmed(isDepositsConfirmed)
.setIsDepositsUnlocked(isDepositsUnlocked) .setIsDepositsUnlocked(isDepositsUnlocked)
.setIsDepositsFinalized(isDepositsFinalized)
.setIsPaymentSent(isPaymentSent) .setIsPaymentSent(isPaymentSent)
.setIsPaymentReceived(isPaymentReceived) .setIsPaymentReceived(isPaymentReceived)
.setIsCompleted(isCompleted) .setIsCompleted(isCompleted)
.setIsPayoutPublished(isPayoutPublished) .setIsPayoutPublished(isPayoutPublished)
.setIsPayoutConfirmed(isPayoutConfirmed) .setIsPayoutConfirmed(isPayoutConfirmed)
.setIsPayoutUnlocked(isPayoutUnlocked) .setIsPayoutUnlocked(isPayoutUnlocked)
.setIsPayoutFinalized(isPayoutFinalized)
.setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContractAsJson(contractAsJson == null ? "" : contractAsJson)
.setContract(contract.toProtoMessage()) .setContract(contract.toProtoMessage())
.setStartTime(startTime)
.setMaxDurationMs(maxDurationMs)
.setDeadlineTime(deadlineTime)
.build(); .build();
} }
@ -287,14 +307,19 @@ public class TradeInfo implements Payload {
.withIsDepositsPublished(proto.getIsDepositsPublished()) .withIsDepositsPublished(proto.getIsDepositsPublished())
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed()) .withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked()) .withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
.withIsDepositsFinalized(proto.getIsDepositsFinalized())
.withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentSent(proto.getIsPaymentSent())
.withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsPaymentReceived(proto.getIsPaymentReceived())
.withIsCompleted(proto.getIsCompleted()) .withIsCompleted(proto.getIsCompleted())
.withIsPayoutPublished(proto.getIsPayoutPublished()) .withIsPayoutPublished(proto.getIsPayoutPublished())
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed()) .withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked()) .withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
.withIsPayoutFinalized(proto.getIsPayoutFinalized())
.withContractAsJson(proto.getContractAsJson()) .withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract()))) .withContract((ContractInfo.fromProto(proto.getContract())))
.withStartTime(proto.getStartTime())
.withMaxDurationMs(proto.getMaxDurationMs())
.withDeadlineTime(proto.getDeadlineTime())
.build(); .build();
} }
@ -330,15 +355,20 @@ public class TradeInfo implements Payload {
", isDepositsPublished=" + isDepositsPublished + "\n" + ", isDepositsPublished=" + isDepositsPublished + "\n" +
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" + ", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" + ", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
", isDepositsFinalized=" + isDepositsFinalized + "\n" +
", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" +
", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" +
", isPayoutPublished=" + isPayoutPublished + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" +
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" + ", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" + ", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
", isPayoutFinalized=" + isPayoutFinalized + "\n" +
", isCompleted=" + isCompleted + "\n" + ", isCompleted=" + isCompleted + "\n" +
", offer=" + offer + "\n" + ", offer=" + offer + "\n" +
", contractAsJson=" + contractAsJson + "\n" + ", contractAsJson=" + contractAsJson + "\n" +
", contract=" + contract + "\n" + ", contract=" + contract + "\n" +
", startTime=" + startTime + "\n" +
", maxDurationMs=" + maxDurationMs + "\n" +
", deadlineTime=" + deadlineTime + "\n" +
'}'; '}';
} }
} }

View file

@ -64,15 +64,20 @@ public final class TradeInfoV1Builder {
private boolean isDepositsPublished; private boolean isDepositsPublished;
private boolean isDepositsConfirmed; private boolean isDepositsConfirmed;
private boolean isDepositsUnlocked; private boolean isDepositsUnlocked;
private boolean isDepositsFinalized;
private boolean isPaymentSent; private boolean isPaymentSent;
private boolean isPaymentReceived; private boolean isPaymentReceived;
private boolean isPayoutPublished; private boolean isPayoutPublished;
private boolean isPayoutConfirmed; private boolean isPayoutConfirmed;
private boolean isPayoutUnlocked; private boolean isPayoutUnlocked;
private boolean isPayoutFinalized;
private boolean isCompleted; private boolean isCompleted;
private String contractAsJson; private String contractAsJson;
private ContractInfo contract; private ContractInfo contract;
private String closingStatus; private String closingStatus;
private long startTime;
private long maxDurationMs;
private long deadlineTime;
public TradeInfoV1Builder withOffer(OfferInfo offer) { public TradeInfoV1Builder withOffer(OfferInfo offer) {
this.offer = offer; this.offer = offer;
@ -239,6 +244,11 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) {
this.isDepositsFinalized = isDepositsFinalized;
return this;
}
public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) { public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) {
this.isPaymentSent = isPaymentSent; this.isPaymentSent = isPaymentSent;
return this; return this;
@ -264,6 +274,11 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) {
this.isPayoutFinalized = isPayoutFinalized;
return this;
}
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) { public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
this.isCompleted = isCompleted; this.isCompleted = isCompleted;
return this; return this;
@ -284,6 +299,21 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withStartTime(long startTime) {
this.startTime = startTime;
return this;
}
public TradeInfoV1Builder withMaxDurationMs(long maxDurationMs) {
this.maxDurationMs = maxDurationMs;
return this;
}
public TradeInfoV1Builder withDeadlineTime(long deadlineTime) {
this.deadlineTime = deadlineTime;
return this;
}
public TradeInfo build() { public TradeInfo build() {
return new TradeInfo(this); return new TradeInfo(this);
} }

View file

@ -75,7 +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.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); 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));

View file

@ -55,7 +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.XmrConnectionError; 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;
@ -159,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler; rejectedTxErrorMessageHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler; private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler; private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -431,9 +431,9 @@ public class HavenoSetup {
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
// listen for fallback handling // listen for fallback handling
getConnectionServiceError().addListener((observable, oldValue, newValue) -> { getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionErrorHandler == null) return; if (displayMoneroConnectionFallbackHandler == null) return;
displayMoneroConnectionErrorHandler.accept(newValue); displayMoneroConnectionFallbackHandler.accept(newValue);
}); });
log.info("Init P2P network"); log.info("Init P2P network");
@ -735,8 +735,8 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg(); return xmrConnectionService.getConnectionServiceErrorMsg();
} }
public ObjectProperty<XmrConnectionError> getConnectionServiceError() { public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceError(); return xmrConnectionService.getConnectionServiceFallbackType();
} }
public StringProperty getTopErrorMsg() { public StringProperty getTopErrorMsg() {

View file

@ -94,7 +94,7 @@ public class P2PNetworkSetup {
if (warning != null && p2pPeers == 0) { if (warning != null && p2pPeers == 0) {
result = warning; result = warning;
} else { } else {
String p2pInfo = ((int) numXmrPeers > 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers); String p2pInfo = ((int) numXmrPeers >= 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers);
if (dataReceived && hiddenService) { if (dataReceived && hiddenService) {
result = p2pInfo; result = p2pInfo;
} else if (p2pPeers == 0) } else if (p2pPeers == 0)

View file

@ -188,6 +188,8 @@ public class WalletAppSetup {
} else { } else {
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage())); xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage()));
} }
} else {
xmrConnectionService.getConnectionServiceErrorMsg().set(errorMsg);
} }
} }
return result; return result;

View file

@ -151,6 +151,7 @@ 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 // shut down trade and wallet services
log.info("Shutting down trade and wallet services"); log.info("Shutting down trade and wallet services");
@ -161,13 +162,12 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
injector.getInstance(XmrConnectionService.class).shutDown(); injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown();
}); });
});
// we wait max 5 sec. // we wait max 5 sec.
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
PersistenceManager.flushAllDataToDiskAtShutdown(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
resultHandler.handleResult(); resultHandler.handleResult();
log.info("Graceful shutdown caused a timeout. Exiting now."); log.warn("Graceful shutdown caused a timeout. Exiting now.");
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
}); });
}, 5); }, 5);

View file

@ -84,9 +84,9 @@ public class FilterManager {
private final ConfigFileEditor configFileEditor; private final ConfigFileEditor configFileEditor;
private final ProvidersRepository providersRepository; private final ProvidersRepository providersRepository;
private final boolean ignoreDevMsg; private final boolean ignoreDevMsg;
private final boolean useDevPrivilegeKeys;
private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>(); private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private final List<String> publicKeys;
private ECKey filterSigningKey; private ECKey filterSigningKey;
private final Set<Filter> invalidFilters = new HashSet<>(); private final Set<Filter> invalidFilters = new HashSet<>();
private Consumer<String> filterWarningHandler; private Consumer<String> filterWarningHandler;
@ -113,16 +113,31 @@ public class FilterManager {
this.configFileEditor = new ConfigFileEditor(config.configFile); this.configFileEditor = new ConfigFileEditor(config.configFile);
this.providersRepository = providersRepository; this.providersRepository = providersRepository;
this.ignoreDevMsg = ignoreDevMsg; this.ignoreDevMsg = ignoreDevMsg;
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
publicKeys = useDevPrivilegeKeys ?
Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) :
List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27",
"029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f",
"034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9");
banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork); banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork);
} }
protected List<String> getPubKeyList() {
switch (Config.baseCurrencyNetwork()) {
case XMR_LOCAL:
if (useDevPrivilegeKeys) return Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY);
return List.of(
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492",
"026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519");
case XMR_STAGENET:
return List.of(
"03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
"02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
"0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
case XMR_MAINNET:
return List.of();
default:
throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork());
}
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// API // API
@ -587,16 +602,16 @@ public class FilterManager {
"but the new version does not recognize it as valid filter): " + "but the new version does not recognize it as valid filter): " +
"signerPubKeyAsHex from filter is not part of our pub key list. " + "signerPubKeyAsHex from filter is not part of our pub key list. " +
"signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}", "signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}",
signerPubKeyAsHex, publicKeys, new Date(filter.getCreationDate())); signerPubKeyAsHex, getPubKeyList(), new Date(filter.getCreationDate()));
return false; return false;
} }
return true; return true;
} }
private boolean isPublicKeyInList(String pubKeyAsHex) { private boolean isPublicKeyInList(String pubKeyAsHex) {
boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); boolean isPublicKeyInList = getPubKeyList().contains(pubKeyAsHex);
if (!isPublicKeyInList) { if (!isPublicKeyInList) {
log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, getPubKeyList());
} }
return isPublicKeyInList; return isPublicKeyInList;
} }

View file

@ -54,7 +54,7 @@ public final class CryptoCurrency extends TradeCurrency {
public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) { public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) {
return new CryptoCurrency(proto.getCode(), return new CryptoCurrency(proto.getCode(),
proto.getName(), CurrencyUtil.getNameByCode(proto.getCode()),
proto.getCryptoCurrency().getIsAsset()); proto.getCryptoCurrency().getIsAsset());
} }

View file

@ -66,7 +66,7 @@ import static java.lang.String.format;
@Slf4j @Slf4j
public class CurrencyUtil { public class CurrencyUtil {
public static void setup() { public static void setup() {
setBaseCurrencyCode("XMR"); setBaseCurrencyCode(baseCurrencyCode);
} }
private static final AssetRegistry assetRegistry = new AssetRegistry(); private static final AssetRegistry assetRegistry = new AssetRegistry();
@ -198,12 +198,16 @@ public class CurrencyUtil {
final List<CryptoCurrency> result = new ArrayList<>(); final List<CryptoCurrency> result = new ArrayList<>();
result.add(new CryptoCurrency("BTC", "Bitcoin")); result.add(new CryptoCurrency("BTC", "Bitcoin"));
result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("DOGE", "Dogecoin"));
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("XRP", "Ripple"));
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); result.add(new CryptoCurrency("ADA", "Cardano"));
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); result.add(new CryptoCurrency("SOL", "Solana"));
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); result.add(new CryptoCurrency("TRX", "Tron"));
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin"));
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD"));
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin"));
result.sort(TradeCurrency::compareTo); result.sort(TradeCurrency::compareTo);
return result; return result;
} }
@ -284,7 +288,7 @@ public class CurrencyUtil {
} }
/** /**
* We return true if it is BTC or any of our currencies available in the assetRegistry. * We return true if it is XMR or any of our currencies available in the assetRegistry.
* For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then. * For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then.
* As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found * As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found
* we return true as well in case we have no traditional currency for the code. * we return true as well in case we have no traditional currency for the code.
@ -406,6 +410,13 @@ public class CurrencyUtil {
removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na");
return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset); return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset);
} }
if (isTraditionalNonFiatCurrency(currencyCode)) {
return getTraditionalNonFiatCurrencies().stream()
.filter(currency -> currency.getCode().equals(currencyCode))
.findAny()
.map(TradeCurrency::getName)
.orElse(currencyCode);
}
try { try {
return Currency.getInstance(currencyCode).getDisplayName(); return Currency.getInstance(currencyCode).getDisplayName();
} catch (Throwable t) { } catch (Throwable t) {
@ -507,17 +518,11 @@ public class CurrencyUtil {
} }
public static String getCurrencyPair(String currencyCode) { public static String getCurrencyPair(String currencyCode) {
if (isTraditionalCurrency(currencyCode))
return Res.getBaseCurrencyCode() + "/" + currencyCode; return Res.getBaseCurrencyCode() + "/" + currencyCode;
else
return currencyCode + "/" + Res.getBaseCurrencyCode();
} }
public static String getCounterCurrency(String currencyCode) { public static String getCounterCurrency(String currencyCode) {
if (isTraditionalCurrency(currencyCode))
return currencyCode; return currencyCode;
else
return Res.getBaseCurrencyCode();
} }
public static String getPriceWithCurrencyCode(String currencyCode) { public static String getPriceWithCurrencyCode(String currencyCode) {
@ -525,9 +530,6 @@ public class CurrencyUtil {
} }
public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) { public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) {
if (isCryptoCurrency(currencyCode))
return Res.get(translationKey, Res.getBaseCurrencyCode(), currencyCode);
else
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode()); return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
} }

View file

@ -103,7 +103,11 @@ public class Res {
} }
public static String get(String key, Object... arguments) { public static String get(String key, Object... arguments) {
return MessageFormat.format(Res.get(key), arguments); return MessageFormat.format(escapeQuotes(get(key)), arguments);
}
private static String escapeQuotes(String s) {
return s.replace("'", "''");
} }
public static String get(String key) { public static String get(String key) {

View file

@ -86,7 +86,7 @@ public final class TraditionalCurrency extends TradeCurrency {
} }
public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) { public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) {
return new TraditionalCurrency(proto.getCode(), proto.getName()); return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode()));
} }

View file

@ -32,7 +32,7 @@ public class CryptoExchangeRate {
*/ */
public final Coin coin; public final Coin coin;
public final CryptoMoney crypto; public final CryptoMoney cryptoMoney;
/** /**
* Construct exchange rate. This amount of coin is worth that amount of crypto. * Construct exchange rate. This amount of coin is worth that amount of crypto.
@ -43,7 +43,7 @@ public class CryptoExchangeRate {
checkArgument(crypto.isPositive()); checkArgument(crypto.isPositive());
checkArgument(crypto.currencyCode != null, "currency code required"); checkArgument(crypto.currencyCode != null, "currency code required");
this.coin = coin; this.coin = coin;
this.crypto = crypto; this.cryptoMoney = crypto;
} }
/** /**
@ -59,13 +59,13 @@ public class CryptoExchangeRate {
* @throws ArithmeticException if the converted crypto amount is too high or too low. * @throws ArithmeticException if the converted crypto amount is too high or too low.
*/ */
public CryptoMoney coinToCrypto(Coin convertCoin) { public CryptoMoney coinToCrypto(Coin convertCoin) {
BigInteger converted = BigInteger.valueOf(coin.value) final BigInteger converted = BigInteger.valueOf(convertCoin.value)
.multiply(BigInteger.valueOf(convertCoin.value)) .multiply(BigInteger.valueOf(cryptoMoney.value))
.divide(BigInteger.valueOf(crypto.value)); .divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow"); throw new ArithmeticException("Overflow");
return CryptoMoney.valueOf(crypto.currencyCode, converted.longValue()); return CryptoMoney.valueOf(cryptoMoney.currencyCode, converted.longValue());
} }
/** /**
@ -74,12 +74,11 @@ public class CryptoExchangeRate {
* @throws ArithmeticException if the converted coin amount is too high or too low. * @throws ArithmeticException if the converted coin amount is too high or too low.
*/ */
public Coin cryptoToCoin(CryptoMoney convertCrypto) { public Coin cryptoToCoin(CryptoMoney convertCrypto) {
checkArgument(convertCrypto.currencyCode.equals(crypto.currencyCode), "Currency mismatch: %s vs %s", checkArgument(convertCrypto.currencyCode.equals(cryptoMoney.currencyCode), "Currency mismatch: %s vs %s",
convertCrypto.currencyCode, crypto.currencyCode); convertCrypto.currencyCode, cryptoMoney.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing. // Use BigInteger because it's much easier to maintain full precision without overflowing.
BigInteger converted = BigInteger.valueOf(crypto.value) final BigInteger converted = BigInteger.valueOf(convertCrypto.value).multiply(BigInteger.valueOf(coin.value))
.multiply(BigInteger.valueOf(convertCrypto.value)) .divide(BigInteger.valueOf(cryptoMoney.value));
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow"); throw new ArithmeticException("Overflow");

View file

@ -136,7 +136,7 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
public String toFriendlyString() { public String toFriendlyString() {
return monetary instanceof CryptoMoney ? return monetary instanceof CryptoMoney ?
((CryptoMoney) monetary).toFriendlyString() + "/XMR" : ((CryptoMoney) monetary).toFriendlyString().replace(((CryptoMoney) monetary).currencyCode, "") + "XMR/" + ((CryptoMoney) monetary).currencyCode :
((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode; ((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode;
} }

View file

@ -15,21 +15,21 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>. * along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/ */
package haveno.core.monetary; package haveno.core.monetary;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.common.base.Objects; import com.google.common.base.Objects;
/** /**
* An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount. * An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount.
*/ */
public class TraditionalExchangeRate implements Serializable { public class TraditionalExchangeRate implements Serializable {
public final Coin coin; public final Coin coin;
public final TraditionalMoney traditionalMoney; public final TraditionalMoney traditionalMoney;
@ -54,7 +54,8 @@
*/ */
public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) { public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) {
// Use BigInteger because it's much easier to maintain full precision without overflowing. // Use BigInteger because it's much easier to maintain full precision without overflowing.
final BigInteger converted = BigInteger.valueOf(convertCoin.value).multiply(BigInteger.valueOf(traditionalMoney.value)) final BigInteger converted = BigInteger.valueOf(convertCoin.value)
.multiply(BigInteger.valueOf(traditionalMoney.value))
.divide(BigInteger.valueOf(coin.value)); .divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
@ -94,5 +95,4 @@
public int hashCode() { public int hashCode() {
return Objects.hashCode(coin, traditionalMoney); return Objects.hashCode(coin, traditionalMoney);
} }
} }

View file

@ -23,5 +23,6 @@ public enum MessageState {
ARRIVED, ARRIVED,
STORED_IN_MAILBOX, STORED_IN_MAILBOX,
ACKNOWLEDGED, ACKNOWLEDGED,
FAILED FAILED,
NACKED
} }

View file

@ -59,7 +59,7 @@ public class TradeEvents {
} }
private void setTradePhaseListener(Trade trade) { private void setTradePhaseListener(Trade trade) {
if (isInitialized) log.info("We got a new trade. id={}", trade.getId()); if (isInitialized) log.info("We got a new trade, tradeId={}", trade.getId(), "hasBuyerAsTakerWithoutDeposit=" + trade.getOffer().hasBuyerAsTakerWithoutDeposit());
if (!trade.isPayoutPublished()) { if (!trade.isPayoutPublished()) {
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
String msg = null; String msg = null;
@ -70,6 +70,7 @@ public class TradeEvents {
case DEPOSITS_PUBLISHED: case DEPOSITS_PUBLISHED:
break; break;
case DEPOSITS_UNLOCKED: case DEPOSITS_UNLOCKED:
case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized?
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.conf", shortId); msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
break; break;

View file

@ -110,13 +110,12 @@ public class MarketAlerts {
} }
private void onOfferAdded(Offer offer) { private void onOfferAdded(Offer offer) {
String currencyCode = offer.getCurrencyCode(); String currencyCode = offer.getCounterCurrencyCode();
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
Price offerPrice = offer.getPrice(); Price offerPrice = offer.getPrice();
if (marketPrice != null && offerPrice != null) { if (marketPrice != null && offerPrice != null) {
boolean isSellOffer = offer.getDirection() == OfferDirection.SELL; boolean isSellOffer = offer.getDirection() == OfferDirection.SELL;
String shortOfferId = offer.getShortId(); String shortOfferId = offer.getShortId();
boolean isTraditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
String alertId = getAlertId(offer); String alertId = getAlertId(offer);
user.getMarketAlertFilters().stream() user.getMarketAlertFilters().stream()
.filter(marketAlertFilter -> !offer.isMyOffer(keyRing)) .filter(marketAlertFilter -> !offer.isMyOffer(keyRing))
@ -133,9 +132,7 @@ public class MarketAlerts {
double offerPriceValue = offerPrice.getValue(); double offerPriceValue = offerPrice.getValue();
double ratio = offerPriceValue / marketPriceAsDouble; double ratio = offerPriceValue / marketPriceAsDouble;
ratio = 1 - ratio; ratio = 1 - ratio;
if (isTraditionalCurrency && isSellOffer) if (isSellOffer)
ratio *= -1;
else if (!isTraditionalCurrency && !isSellOffer)
ratio *= -1; ratio *= -1;
ratio = ratio * 10000; ratio = ratio * 10000;
@ -148,7 +145,6 @@ public class MarketAlerts {
if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) { if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) {
String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy"); String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy");
String marketDir; String marketDir;
if (isTraditionalCurrency) {
if (isSellOffer) { if (isSellOffer) {
marketDir = ratio > 0 ? marketDir = ratio > 0 ?
Res.get("account.notifications.marketAlert.message.msg.above") : Res.get("account.notifications.marketAlert.message.msg.above") :
@ -158,17 +154,6 @@ public class MarketAlerts {
Res.get("account.notifications.marketAlert.message.msg.above") : Res.get("account.notifications.marketAlert.message.msg.above") :
Res.get("account.notifications.marketAlert.message.msg.below"); Res.get("account.notifications.marketAlert.message.msg.below");
} }
} else {
if (isSellOffer) {
marketDir = ratio < 0 ?
Res.get("account.notifications.marketAlert.message.msg.above") :
Res.get("account.notifications.marketAlert.message.msg.below");
} else {
marketDir = ratio > 0 ?
Res.get("account.notifications.marketAlert.message.msg.above") :
Res.get("account.notifications.marketAlert.message.msg.below");
}
}
ratio = Math.abs(ratio); ratio = Math.abs(ratio);
String msg = Res.get("account.notifications.marketAlert.message.msg", String msg = Res.get("account.notifications.marketAlert.message.msg",

View file

@ -22,7 +22,6 @@ import com.google.inject.Singleton;
import haveno.common.app.Version; import haveno.common.app.Version;
import haveno.common.crypto.PubKeyRingProvider; import haveno.common.crypto.PubKeyRingProvider;
import haveno.common.util.Utilities; import haveno.common.util.Utilities;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.monetary.Price; import haveno.core.monetary.Price;
import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccount;
@ -35,6 +34,7 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.trade.statistics.TradeStatisticsManager;
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;
@ -92,7 +92,6 @@ 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,
@ -134,10 +133,12 @@ public class CreateOfferService {
// must nullify empty string so contracts match // must nullify empty string so contracts match
if ("".equals(extraInfo)) extraInfo = null; if ("".equals(extraInfo)) extraInfo = null;
// verify buyer as taker security deposit // verify config for private no deposit offers
boolean isBuyerMaker = offerUtil.isBuyOffer(direction); boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { if (buyerAsTakerWithoutDeposit || isPrivateOffer) {
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers"); if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit");
if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers");
if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit");
} }
// verify fixed price xor market price with margin // verify fixed price xor market price with margin
@ -149,15 +150,16 @@ public class CreateOfferService {
// verify price // verify price
boolean useMarketBasedPriceValue = fixedPrice == null && boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice && useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) && isExternalPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
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 // adjust amount and min amount
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit);
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId()); amount = CoinUtil.getRoundedAmount(amount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
// generate one-time challenge for private offer // generate one-time challenge for private offer
String challenge = null; String challenge = null;
@ -173,16 +175,15 @@ public class CreateOfferService {
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
long amountAsLong = amount != null ? amount.longValueExact() : 0L; long amountAsLong = amount != null ? amount.longValueExact() : 0L;
long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L; long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L;
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); String baseCurrencyCode = Res.getBaseCurrencyCode();
String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode(); String counterCurrencyCode = currencyCode;
String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount); String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount); List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
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; boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact();
boolean useAutoClose = false; boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false; boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0; long lowerClosePrice = 0;
@ -204,8 +205,8 @@ public class CreateOfferService {
useMarketBasedPriceValue, useMarketBasedPriceValue,
amountAsLong, amountAsLong,
minAmountAsLong, minAmountAsLong,
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT, HavenoUtils.getMakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT, HavenoUtils.getTakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
HavenoUtils.PENALTY_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
securityDepositPct, securityDepositPct,
@ -219,7 +220,7 @@ public class CreateOfferService {
acceptedBanks, acceptedBanks,
Version.VERSION, Version.VERSION,
xmrWalletService.getHeight(), xmrWalletService.getHeight(),
maxTradeLimit, maxTradeLimitAsLong,
maxTradePeriod, maxTradePeriod,
useAutoClose, useAutoClose,
useReOpenAfterAutoClose, useReOpenAfterAutoClose,
@ -239,7 +240,6 @@ public class CreateOfferService {
return offer; return offer;
} }
// TODO: add trigger price?
public Offer createClonedOffer(Offer sourceOffer, public Offer createClonedOffer(Offer sourceOffer,
String currencyCode, String currencyCode,
Price fixedPrice, Price fixedPrice,
@ -336,7 +336,7 @@ public class CreateOfferService {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private boolean isMarketPriceAvailable(String currencyCode) { private boolean isExternalPriceAvailable(String currencyCode) {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice(); return marketPrice != null && marketPrice.isExternallyProvidedPrice();
} }

View file

@ -39,7 +39,9 @@ import haveno.core.payment.payload.PaymentMethod;
import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.MarketPrice;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.util.PriceUtil;
import haveno.core.util.VolumeUtil; import haveno.core.util.VolumeUtil;
import haveno.core.util.coin.CoinUtil;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringProperty;
@ -173,32 +175,27 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Nullable @Nullable
public Price getPrice() { public Price getPrice() {
String currencyCode = getCurrencyCode(); String counterCurrencyCode = getCounterCurrencyCode();
if (!offerPayload.isUseMarketBasedPrice()) { if (!offerPayload.isUseMarketBasedPrice()) {
return Price.valueOf(currencyCode, offerPayload.getPrice()); return Price.valueOf(counterCurrencyCode, isInverted() ? PriceUtil.invertLongPrice(offerPayload.getPrice(), counterCurrencyCode) : offerPayload.getPrice());
} }
checkNotNull(priceFeedService, "priceFeed must not be null"); checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); MarketPrice marketPrice = priceFeedService.getMarketPrice(counterCurrencyCode);
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
double factor; double factor;
double marketPriceMargin = offerPayload.getMarketPriceMarginPct(); double marketPriceMargin = offerPayload.getMarketPriceMarginPct();
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
factor = getDirection() == OfferDirection.SELL ?
1 - marketPriceMargin : 1 + marketPriceMargin;
} else {
factor = getDirection() == OfferDirection.BUY ? factor = getDirection() == OfferDirection.BUY ?
1 - marketPriceMargin : 1 + marketPriceMargin; 1 - marketPriceMargin : 1 + marketPriceMargin;
}
double marketPriceAsDouble = marketPrice.getPrice(); double marketPriceAsDouble = marketPrice.getPrice();
double targetPriceAsDouble = marketPriceAsDouble * factor; double targetPriceAsDouble = marketPriceAsDouble * factor;
try { try {
int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? int precision = CurrencyUtil.isTraditionalCurrency(counterCurrencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT; CryptoMoney.SMALLEST_UNIT_EXPONENT;
double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision);
final long roundedToLong = MathUtils.roundDoubleToLong(scaled); final long roundedToLong = MathUtils.roundDoubleToLong(scaled);
return Price.valueOf(currencyCode, roundedToLong); return Price.valueOf(counterCurrencyCode, roundedToLong);
} catch (Exception e) { } catch (Exception e) {
log.error("Exception at getPrice / parseToFiat: " + e + "\n" + log.error("Exception at getPrice / parseToFiat: " + e + "\n" +
"That case should never happen."); "That case should never happen.");
@ -224,7 +221,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
return; return;
} }
Price tradePrice = Price.valueOf(getCurrencyCode(), price); Price tradePrice = Price.valueOf(getCounterCurrencyCode(), price);
Price offerPrice = getPrice(); Price offerPrice = getPrice();
if (offerPrice == null) if (offerPrice == null)
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
@ -239,7 +236,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
double deviation = Math.abs(1 - relation); double deviation = Math.abs(1 - relation);
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
getShortId(), getCurrencyCode(), price, offerPrice.getValue(), getShortId(), getCounterCurrencyCode(), price, offerPrice.getValue(),
deviation * 100 + "%"); deviation * 100 + "%");
if (deviation > PRICE_TOLERANCE) { if (deviation > PRICE_TOLERANCE) {
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
@ -251,12 +248,13 @@ public class Offer implements NetworkPayload, PersistablePayload {
} }
@Nullable @Nullable
public Volume getVolumeByAmount(BigInteger amount) { public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) {
Price price = getPrice(); Price price = getPrice();
if (price == null || amount == null) { if (price == null || amount == null) {
return null; return null;
} }
Volume volumeByAmount = price.getVolumeByAmount(amount); BigInteger adjustedAmount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, getCounterCurrencyCode(), getPaymentMethodId());
Volume volumeByAmount = price.getVolumeByAmount(adjustedAmount);
volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId()); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId());
return volumeByAmount; return volumeByAmount;
@ -385,12 +383,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Nullable @Nullable
public Volume getVolume() { public Volume getVolume() {
return getVolumeByAmount(getAmount()); return getVolumeByAmount(getAmount(), getMinAmount(), getAmount());
} }
@Nullable @Nullable
public Volume getMinVolume() { public Volume getMinVolume() {
return getVolumeByAmount(getMinAmount()); return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount());
} }
public boolean isBuyOffer() { public boolean isBuyOffer() {
@ -507,23 +505,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getCountryCode(); return offerPayload.getCountryCode();
} }
public String getCurrencyCode() { public String getBaseCurrencyCode() {
if (currencyCode != null) { return isInverted() ? offerPayload.getCounterCurrencyCode() : offerPayload.getBaseCurrencyCode(); // legacy offers inverted crypto
return currencyCode;
}
currencyCode = offerPayload.getBaseCurrencyCode().equals("XMR") ?
offerPayload.getCounterCurrencyCode() :
offerPayload.getBaseCurrencyCode();
return currencyCode;
} }
public String getCounterCurrencyCode() { public String getCounterCurrencyCode() {
return offerPayload.getCounterCurrencyCode(); if (currencyCode != null) return currencyCode;
currencyCode = isInverted() ? offerPayload.getBaseCurrencyCode() : offerPayload.getCounterCurrencyCode(); // legacy offers inverted crypto
return currencyCode;
} }
public String getBaseCurrencyCode() { public boolean isInverted() {
return offerPayload.getBaseCurrencyCode(); return !offerPayload.getBaseCurrencyCode().equals("XMR");
} }
public String getPaymentMethodId() { public String getPaymentMethodId() {
@ -584,21 +577,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.isUseReOpenAfterAutoClose(); return offerPayload.isUseReOpenAfterAutoClose();
} }
public boolean isXmrAutoConf() {
if (!isXmr()) {
return false;
}
if (getExtraDataMap() == null || !getExtraDataMap().containsKey(OfferPayload.XMR_AUTO_CONF)) {
return false;
}
return getExtraDataMap().get(OfferPayload.XMR_AUTO_CONF).equals(OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE);
}
public boolean isXmr() {
return getCurrencyCode().equals("XMR");
}
public boolean isTraditionalOffer() { public boolean isTraditionalOffer() {
return CurrencyUtil.isTraditionalCurrency(currencyCode); return CurrencyUtil.isTraditionalCurrency(currencyCode);
} }

View file

@ -149,6 +149,20 @@ public class OfferBookService {
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
announceOfferRemoved(offer); announceOfferRemoved(offer);
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
} }
}); });
}, OfferBookService.class.getSimpleName()); }, OfferBookService.class.getSimpleName());
@ -257,7 +271,7 @@ public class OfferBookService {
public List<Offer> getOffersByCurrency(String direction, String currencyCode) { public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
return getOffers().stream() return getOffers().stream()
.filter(o -> o.getOfferPayload().getBaseCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction) .filter(o -> o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -298,20 +312,6 @@ public class OfferBookService {
synchronized (offerBookChangedListeners) { synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); 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
}
}
}
} }
private boolean hasValidOffer(String offerId) { private boolean hasValidOffer(String offerId) {
@ -404,7 +404,7 @@ public class OfferBookService {
} }
// validate max offers with same key images // 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()); if (numOffersWithSharedKeyImages > Restrictions.getMaxOffersWithSharedFunds()) throw new RuntimeException("More than " + Restrictions.getMaxOffersWithSharedFunds() + " offers exist with same same key images as new offerId=" + offerPayload.getId());
} }
} }
@ -445,11 +445,11 @@ public class OfferBookService {
// We filter the case that it is a MarketBasedPrice but the price is not available // We filter the case that it is a MarketBasedPrice but the price is not available
// That should only be possible if the price feed provider is not available // That should only be possible if the price feed provider is not available
final List<OfferForJson> offerForJsonList = getOffers().stream() final List<OfferForJson> offerForJsonList = getOffers().stream()
.filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCurrencyCode()) != null) .filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCounterCurrencyCode()) != null)
.map(offer -> { .map(offer -> {
try { try {
return new OfferForJson(offer.getDirection(), return new OfferForJson(offer.getDirection(),
offer.getCurrencyCode(), offer.getCounterCurrencyCode(),
offer.getMinAmount(), offer.getMinAmount(),
offer.getAmount(), offer.getAmount(),
offer.getPrice(), offer.getPrice(),

View file

@ -127,6 +127,9 @@ public class OfferFilterService {
if (isMyInsufficientTradeLimit(offer)) { if (isMyInsufficientTradeLimit(offer)) {
return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT;
} }
if (!hasValidArbitrator(offer)) {
return Result.ARBITRATOR_NOT_VALIDATED;
}
if (!hasValidSignature(offer)) { if (!hasValidSignature(offer)) {
return Result.SIGNATURE_NOT_VALIDATED; return Result.SIGNATURE_NOT_VALIDATED;
} }
@ -159,7 +162,7 @@ public class OfferFilterService {
} }
public boolean isCurrencyBanned(Offer offer) { public boolean isCurrencyBanned(Offer offer) {
return filterManager.isCurrencyBanned(offer.getCurrencyCode()); return filterManager.isCurrencyBanned(offer.getCounterCurrencyCode());
} }
public boolean isPaymentMethodBanned(Offer offer) { public boolean isPaymentMethodBanned(Offer offer) {
@ -201,7 +204,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.hasBuyerAsTakerWithoutDeposit())) offer.getCounterCurrencyCode(), 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={}, ",
@ -215,27 +218,28 @@ public class OfferFilterService {
return result; return result;
} }
private boolean hasValidSignature(Offer offer) { private boolean hasValidArbitrator(Offer offer) {
Arbitrator arbitrator = getArbitrator(offer);
return arbitrator != null;
}
// get accepted arbitrator by address private Arbitrator getArbitrator(Offer offer) {
// get arbitrator by address
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
if (arbitrator != null) return arbitrator;
// accepted arbitrator is null if we are the signing arbitrator // check if we are the signing arbitrator
if (arbitrator == null && offer.getOfferPayload().getArbitratorSigner() != null) {
Arbitrator thisArbitrator = user.getRegisteredArbitrator(); Arbitrator thisArbitrator = user.getRegisteredArbitrator();
if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) { if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) return thisArbitrator;
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
} else {
// // otherwise log warning that arbitrator is unregistered // cannot get arbitrator
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()); return null;
// if (!arbitratorAddresses.isEmpty()) {
// log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
// }
}
} }
if (arbitrator == null) return false; // invalid arbitrator private boolean hasValidSignature(Offer offer) {
Arbitrator arbitrator = getArbitrator(offer);
if (arbitrator == null) return false;
return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator); return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator);
} }

View file

@ -100,14 +100,8 @@ public class OfferForJson {
private void setDisplayStrings() { private void setDisplayStrings() {
try { try {
final Price price = getPrice(); final Price price = getPrice();
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
primaryMarketDirection = direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY;
currencyPair = currencyCode + "/" + Res.getBaseCurrencyCode();
} else {
primaryMarketDirection = direction; primaryMarketDirection = direction;
currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode; currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode;
}
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString(); priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString();
@ -116,7 +110,6 @@ public class OfferForJson {
primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString();
primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString(); primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString();
} else { } else {
// amount and volume is inverted for json
priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString(); priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString();
primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString();
primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString(); primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString();

View file

@ -56,12 +56,13 @@ import haveno.core.payment.PayPalAccount;
import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccount;
import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.MarketPrice;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.ReferralIdService; 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.getMaxSecurityDepositAsPercent; import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositPct;
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent; import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositPct;
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,13 +121,13 @@ public class OfferUtil {
return direction == OfferDirection.BUY; return direction == OfferDirection.BUY;
} }
public long getMaxTradeLimit(PaymentAccount paymentAccount, public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode, String currencyCode,
OfferDirection direction, OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) { boolean buyerAsTakerWithoutDeposit) {
return paymentAccount != null return BigInteger.valueOf(paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
: 0; : 0);
} }
/** /**
@ -239,12 +240,12 @@ public class OfferUtil {
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(securityDeposit <= getMaxSecurityDepositAsPercent(), checkArgument(securityDeposit <= getMaxSecurityDepositPct(),
"securityDeposit must not exceed " + "securityDeposit must not exceed " +
getMaxSecurityDepositAsPercent()); getMaxSecurityDepositPct());
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(), checkArgument(securityDeposit >= getMinSecurityDepositPct(),
"securityDeposit must not be less than " + "securityDeposit must not be less than " +
getMinSecurityDepositAsPercent() + " but was " + securityDeposit); getMinSecurityDepositPct() + " 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()),
@ -263,10 +264,27 @@ public class OfferUtil {
} }
public static boolean isTraditionalOffer(Offer offer) { public static boolean isTraditionalOffer(Offer offer) {
return offer.getBaseCurrencyCode().equals("XMR"); return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode());
} }
public static boolean isCryptoOffer(Offer offer) { public static boolean isCryptoOffer(Offer offer) {
return offer.getCounterCurrencyCode().equals("XMR"); return CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode());
}
public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount,
String currencyCode,
OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
// disallow offers which no buyer can take due to trade limits on release
if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) {
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit));
}
if (paymentAccount != null) {
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit));
} else {
return BigInteger.ZERO;
}
} }
} }

View file

@ -122,16 +122,16 @@ public final class OpenOffer implements Tradable {
this(offer, 0, false); this(offer, 0, false);
} }
public OpenOffer(Offer offer, long triggerPrice) { public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) {
this(offer, triggerPrice, false); this(offer, triggerPrice, reserveExactAmount, null);
} }
public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) { public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount, String groupId) {
this.offer = offer; this.offer = offer;
this.triggerPrice = triggerPrice; this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount; this.reserveExactAmount = reserveExactAmount;
this.challenge = offer.getChallenge(); this.challenge = offer.getChallenge();
this.groupId = UUID.randomUUID().toString(); this.groupId = groupId == null ? UUID.randomUUID().toString() : groupId;
state = State.PENDING; state = State.PENDING;
} }
@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable {
return state == State.AVAILABLE; return state == State.AVAILABLE;
} }
public boolean isReserved() {
return state == State.RESERVED;
}
public boolean isDeactivated() { public boolean isDeactivated() {
return state == State.DEACTIVATED; return state == State.DEACTIVATED;
} }

View file

@ -78,6 +78,7 @@ import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
import haveno.core.util.PriceUtil;
import haveno.core.util.Validator; import haveno.core.util.Validator;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.BtcWalletService;
@ -519,6 +520,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
// cannot set trigger price for fixed price offers
if (triggerPrice != 0 && offer.getOfferPayload().getPrice() != 0) {
errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers.");
return;
}
// check source offer and clone limit // check source offer and clone limit
OpenOffer sourceOffer = null; OpenOffer sourceOffer = null;
if (sourceOfferId != null) { if (sourceOfferId != null) {
@ -526,15 +533,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// get source offer // get source offer
Optional<OpenOffer> sourceOfferOptional = getOpenOffer(sourceOfferId); Optional<OpenOffer> sourceOfferOptional = getOpenOffer(sourceOfferId);
if (!sourceOfferOptional.isPresent()) { if (!sourceOfferOptional.isPresent()) {
errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + ".");
return; return;
} }
sourceOffer = sourceOfferOptional.get(); sourceOffer = sourceOfferOptional.get();
// check clone limit // check clone limit
int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size();
if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) { if (numClones >= Restrictions.getMaxOffersWithSharedFunds()) {
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached."); errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.getMaxOffersWithSharedFunds() + " cloned offers with shared funds reached.");
return; return;
} }
} }
@ -632,7 +639,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void applyTriggerState(OpenOffer openOffer) { private void applyTriggerState(OpenOffer openOffer) {
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) { if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer)) {
openOffer.deactivate(true); openOffer.deactivate(true);
} }
} }
@ -661,7 +668,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
log.info("Canceling open offer: {}", openOffer.getId()); log.info("Canceling open offer: {}", openOffer.getId());
if (!offersToBeEdited.containsKey(openOffer.getId())) { if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isAvailable()) { if (isOnOfferBook(openOffer)) {
openOffer.setState(OpenOffer.State.CANCELED); openOffer.setState(OpenOffer.State.CANCELED);
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> { () -> {
@ -683,6 +690,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
private boolean isOnOfferBook(OpenOffer openOffer) {
return openOffer.isAvailable() || openOffer.isReserved();
}
public void editOpenOfferStart(OpenOffer openOffer, public void editOpenOfferStart(OpenOffer openOffer,
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
@ -1083,6 +1094,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
try { try {
ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user); ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
} catch (Exception e) { } catch (Exception e) {
openOffer.getOffer().setState(Offer.State.INVALID);
errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage()); errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
return; return;
} }
@ -1101,6 +1113,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else { } else {
// validate non-pending state // validate non-pending state
boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
if (!skipValidation) {
try { try {
validateSignedState(openOffer); validateSignedState(openOffer);
resultHandler.handleResult(null); // done processing if non-pending state is valid resultHandler.handleResult(null); // done processing if non-pending state is valid
@ -1114,6 +1128,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
} }
} }
}
// sign and post offer if already funded // sign and post offer if already funded
if (openOffer.getReserveTxHash() != null) { if (openOffer.getReserveTxHash() != null) {
@ -1168,10 +1183,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return; return;
} else if (openOffer.getScheduledTxHashes() == null) { } else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer); scheduleWithEarliestTxs(openOffers, openOffer);
}
resultHandler.handleResult(null); resultHandler.handleResult(null);
return; return;
} }
}
} catch (Exception e) { } catch (Exception e) {
if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e);
errorMessageHandler.handleErrorMessage(e.getMessage()); errorMessageHandler.handleErrorMessage(e.getMessage());
@ -1186,7 +1202,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) { } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) {
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature"); throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature");
} else if (arbitrator == null) { } else if (arbitrator == null) {
throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator"); throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unregistered arbitrator");
} else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) {
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature"); throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature");
} else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) { } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) {
@ -1208,6 +1224,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
// check if split output tx is available for offer // check if split output tx is available for offer
if (splitOutputTx != null) {
if (splitOutputTx.isLocked()) return splitOutputTx; if (splitOutputTx.isLocked()) return splitOutputTx;
else { else {
boolean isAvailable = true; boolean isAvailable = true;
@ -1218,7 +1235,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
else log.warn("Split output tx is no longer available for offer {}", openOffer.getId()); else log.warn("Split output tx {} is no longer available for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
}
} else {
log.warn("Split output tx {} no longer exists for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
} }
} }
@ -1244,7 +1264,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsFailed(false)); // TODO: not using setIsIncoming(true) because split output txs sent to self have false; fix in monero-java?
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>(); Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
for (MoneroTxWallet tx : splitOutputTxs) { for (MoneroTxWallet tx : splitOutputTxs) {
if (tx.getOutputs() != null) { // outputs not available until first confirmation if (tx.getOutputs() != null) { // outputs not available until first confirmation
@ -1267,6 +1287,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery() boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
.setAccountIndex(0) .setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex) .setSubaddressIndex(preferredSubaddressIndex)
.setIsIncoming(true)
.setAmount(amount)).size() > 0); .setAmount(amount)).size() > 0);
return hasExactTransfer; return hasExactTransfer;
} }
@ -1342,7 +1363,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
xmrWalletService.handleWalletError(e, sourceConnection); xmrWalletService.handleWalletError(e, sourceConnection, i + 1);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
@ -1549,8 +1570,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
// verify max length of extra info // verify max length of extra info
if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.getMaxExtraInfoLength()) {
errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length(); errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.getMaxExtraInfoLength() + " but got " + offer.getOfferPayload().getExtraInfo().length();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
@ -1574,21 +1595,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
// verify the max version number
if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) {
errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker and taker fees // verify maker and taker fees
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
if (hasBuyerAsTakerWithoutDeposit) { if (hasBuyerAsTakerWithoutDeposit) {
// verify maker's trade fee // verify maker's trade fee
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) { double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct(); if (offer.getMakerFeePct() != makerFeePct) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
@ -1603,8 +1617,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
// verify maker security deposit // verify maker security deposit
if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) { if (offer.getSellerSecurityDepositPct() != Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
@ -1619,33 +1633,43 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} else { } else {
// verify public offer (remove to generally allow private offers)
if (offer.isPrivateOffer() || offer.getChallengeHash() != null) {
errorMessage = "Private offer " + request.offerId + " is not valid. It must have direction SELL, taker fee of 0, and a challenge hash.";
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's trade fee // verify maker's trade fee
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct(); if (offer.getMakerFeePct() != makerFeePct) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
} }
// verify taker's trade fee // verify taker's trade fee
if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { double takerFeePct = HavenoUtils.getTakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct(); if (offer.getTakerFeePct() != takerFeePct) {
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + takerFeePct + " but got " + offer.getTakerFeePct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
} }
// verify seller's security deposit // verify seller's security deposit
if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { if (offer.getSellerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
} }
// verify buyer's security deposit // verify buyer's security deposit
if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { if (offer.getBuyerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct(); errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getBuyerSecurityDepositPct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
@ -1662,17 +1686,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// verify penalty fee // verify penalty fee
if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) {
errorMessage = "Wrong penalty fee for offer " + request.offerId; errorMessage = "Wrong penalty fee percent for offer " + request.offerId + ". Expected " + HavenoUtils.PENALTY_FEE_PCT + " but got " + offer.getPenaltyFeePct();
log.warn(errorMessage); log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return; return;
} }
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT); double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), makerFeePct);
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, HavenoUtils.PENALTY_FEE_PCT);
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
offer.getId(), offer.getId(),
penaltyFee, penaltyFee,
@ -1696,7 +1721,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
signedOfferPayload.getPubKeyRing().hashCode(), // trader id signedOfferPayload.getPubKeyRing().hashCode(), // trader id
signedOfferPayload.getId(), signedOfferPayload.getId(),
offer.getAmount().longValueExact(), offer.getAmount().longValueExact(),
maxTradeFee.longValueExact(), penaltyFee.longValueExact(),
request.getReserveTxHash(), request.getReserveTxHash(),
request.getReserveTxHex(), request.getReserveTxHex(),
request.getReserveTxKeyImages(), request.getReserveTxKeyImages(),
@ -1736,6 +1761,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
errorMessage = "Exception at handleSignOfferRequest " + e.getMessage(); errorMessage = "Exception at handleSignOfferRequest " + e.getMessage();
log.error(errorMessage + "\n", e); log.error(errorMessage + "\n", e);
} finally { } finally {
if (result == false && errorMessage == null) {
log.warn("Arbitrator is NACKing SignOfferRequest for unknown reason with offerId={}. That should never happen", request.getOfferId());
log.warn("Printing stacktrace:");
Thread.dumpStack();
}
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage);
} }
} }
@ -1925,8 +1955,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
result, result,
errorMessage); errorMessage);
if (ackMessage.isSuccess()) {
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}", log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid()); reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
} else {
log.warn("Sending NACK for {} to peer {} with offerId {} and sourceUid {}, errorMessage={}",
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid(), errorMessage);
}
p2PService.sendEncryptedDirectMessage( p2PService.sendEncryptedDirectMessage(
sender, sender,
senderPubKeyRing, senderPubKeyRing,
@ -1952,8 +1988,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void maybeUpdatePersistedOffers() { private void maybeUpdatePersistedOffers() {
List<OpenOffer> openOffersClone = getOpenOffers();
openOffersClone.forEach(originalOpenOffer -> { // update open offers
List<OpenOffer> updatedOpenOffers = new ArrayList<>();
getOpenOffers().forEach(originalOpenOffer -> {
Offer originalOffer = originalOpenOffer.getOffer(); Offer originalOffer = originalOpenOffer.getOffer();
OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
@ -2000,30 +2038,31 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId()); log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId());
} }
long normalizedPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOfferPayload.getPrice(), originalOffer.getCounterCurrencyCode()) : originalOfferPayload.getPrice();
OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(),
originalOfferPayload.getDate(), originalOfferPayload.getDate(),
ownerNodeAddress, ownerNodeAddress,
originalOfferPayload.getPubKeyRing(), originalOfferPayload.getPubKeyRing(),
originalOfferPayload.getDirection(), originalOfferPayload.getDirection(),
originalOfferPayload.getPrice(), normalizedPrice,
originalOfferPayload.getMarketPriceMarginPct(), originalOfferPayload.getMarketPriceMarginPct(),
originalOfferPayload.isUseMarketBasedPrice(), originalOfferPayload.isUseMarketBasedPrice(),
originalOfferPayload.getAmount(), originalOfferPayload.getAmount(),
originalOfferPayload.getMinAmount(), originalOfferPayload.getMinAmount(),
originalOfferPayload.getMakerFeePct(), originalOfferPayload.getMakerFeePct(),
originalOfferPayload.getTakerFeePct(), originalOfferPayload.getTakerFeePct(),
originalOfferPayload.getPenaltyFeePct(), HavenoUtils.PENALTY_FEE_PCT,
originalOfferPayload.getBuyerSecurityDepositPct(), originalOfferPayload.getBuyerSecurityDepositPct(),
originalOfferPayload.getSellerSecurityDepositPct(), originalOfferPayload.getSellerSecurityDepositPct(),
originalOfferPayload.getBaseCurrencyCode(), originalOffer.getBaseCurrencyCode(),
originalOfferPayload.getCounterCurrencyCode(), originalOffer.getCounterCurrencyCode(),
originalOfferPayload.getPaymentMethodId(), originalOfferPayload.getPaymentMethodId(),
originalOfferPayload.getMakerPaymentAccountId(), originalOfferPayload.getMakerPaymentAccountId(),
originalOfferPayload.getCountryCode(), originalOfferPayload.getCountryCode(),
originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getAcceptedCountryCodes(),
originalOfferPayload.getBankId(), originalOfferPayload.getBankId(),
originalOfferPayload.getAcceptedBankIds(), originalOfferPayload.getAcceptedBankIds(),
originalOfferPayload.getVersionNr(), Version.VERSION,
originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getBlockHeightAtOfferCreation(),
originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradeLimit(),
originalOfferPayload.getMaxTradePeriod(), originalOfferPayload.getMaxTradePeriod(),
@ -2047,13 +2086,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// create new offer // create new offer
Offer updatedOffer = new Offer(updatedPayload); Offer updatedOffer = new Offer(updatedPayload);
updatedOffer.setPriceFeedService(priceFeedService); updatedOffer.setPriceFeedService(priceFeedService);
long normalizedTriggerPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOpenOffer.getTriggerPrice(), originalOffer.getCounterCurrencyCode()) : originalOpenOffer.getTriggerPrice();
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, normalizedTriggerPrice, originalOpenOffer.isReserveExactAmount(), originalOpenOffer.getGroupId());
updatedOpenOffer.setChallenge(originalOpenOffer.getChallenge());
updatedOpenOffers.add(updatedOpenOffer);
}
});
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); // add updated open offers
updatedOpenOffers.forEach(updatedOpenOffer -> {
addOpenOffer(updatedOpenOffer); addOpenOffer(updatedOpenOffer);
requestPersistence(); requestPersistence();
log.info("Updating offer completed. id={}", updatedOpenOffer.getId());
log.info("Updating offer completed. id={}", originalOffer.getId());
}
}); });
} }
@ -2183,6 +2227,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (periodicRefreshOffersTimer == null) if (periodicRefreshOffersTimer == null)
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
if (!stopped) { if (!stopped) {
log.info("Refreshing my open offers");
synchronized (openOffers.getList()) { synchronized (openOffers.getList()) {
int size = openOffers.size(); int size = openOffers.size();
//we clone our list as openOffers might change during our delayed call //we clone our list as openOffers might change during our delayed call

View file

@ -102,7 +102,7 @@ public class TriggerPriceService {
return false; return false;
} }
String currencyCode = openOffer.getOffer().getCurrencyCode(); String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode); boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
int smallestUnitExponent = traditionalCurrency ? int smallestUnitExponent = traditionalCurrency ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT :
@ -116,15 +116,13 @@ public class TriggerPriceService {
OfferDirection direction = openOffer.getOffer().getDirection(); OfferDirection direction = openOffer.getOffer().getDirection();
boolean isSellOffer = direction == OfferDirection.SELL; boolean isSellOffer = direction == OfferDirection.SELL;
boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); return isSellOffer ?
boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency;
return condition ?
marketPriceAsLong < triggerPrice : marketPriceAsLong < triggerPrice :
marketPriceAsLong > triggerPrice; marketPriceAsLong > triggerPrice;
} }
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
String currencyCode = openOffer.getOffer().getCurrencyCode(); String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
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;
@ -162,11 +160,11 @@ public class TriggerPriceService {
private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) { private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> { openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode(); String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>()); openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>());
openOffersByCurrency.get(currencyCode).add(openOffer); openOffersByCurrency.get(currencyCode).add(openOffer);
MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()); MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode());
if (marketPrice != null) { if (marketPrice != null) {
checkPriceThreshold(marketPrice, openOffer); checkPriceThreshold(marketPrice, openOffer);
} }
@ -175,7 +173,7 @@ public class TriggerPriceService {
private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) { private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> { openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode(); String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
if (openOffersByCurrency.containsKey(currencyCode)) { if (openOffersByCurrency.containsKey(currencyCode)) {
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode); Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer); set.remove(openOffer);

View file

@ -72,10 +72,10 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getProtocol().startTimeoutTimer(); model.getProtocol().startTimeoutTimer();
// collect relevant info // collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
BigInteger makerFee = offer.getMaxMakerFee(); BigInteger makerFee = offer.getMaxMakerFee();
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct());
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
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();
@ -100,7 +100,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
throw e; 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, i + 1);
verifyPending(); verifyPending();
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout model.getProtocol().startTimeoutTimer(); // reset protocol timeout

View file

@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -60,8 +61,16 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
try { try {
runInterceptHook(); runInterceptHook();
// create request for arbitrator to sign offer // get payout address entry
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); String returnAddress;
Optional<XmrAddressEntry> addressEntryOpt = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
if (addressEntryOpt.isPresent()) returnAddress = addressEntryOpt.get().getAddressString();
else {
log.warn("Payout address entry found for unsigned offer {} is missing, creating anew", offer.getId());
returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
}
// build sign offer request
SignOfferRequest request = new SignOfferRequest( SignOfferRequest request = new SignOfferRequest(
offer.getId(), offer.getId(),
P2PService.getMyNodeAddress(), P2PService.getMyNodeAddress(),

View file

@ -23,6 +23,7 @@ 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.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.payment.PaymentAccount;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.messages.TradeMessage;
import haveno.core.user.User; import haveno.core.user.User;
@ -96,7 +97,10 @@ 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(), offer.hasBuyerAsTakerWithoutDeposit()); PaymentAccount paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId());
checkArgument(paymentAccount != null, "Payment account is null. makerPaymentAccountId=" + offer.getMakerPaymentAccountId());
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCounterCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
checkArgument(offer.getAmount().longValueExact() <= maxAmount, checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " 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");
@ -108,7 +112,7 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
checkArgument(offer.getDate().getTime() > 0, checkArgument(offer.getDate().getTime() > 0,
"Date must not be 0. date=" + offer.getDate().toString()); "Date must not be 0. date=" + offer.getDate().toString());
checkNotNull(offer.getCurrencyCode(), "Currency is null"); checkNotNull(offer.getCounterCurrencyCode(), "Currency is null");
checkNotNull(offer.getDirection(), "Direction is null"); checkNotNull(offer.getDirection(), "Direction is null");
checkNotNull(offer.getId(), "Id is null"); checkNotNull(offer.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null"); checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");

View file

@ -106,7 +106,7 @@ public class TakeOfferModel implements Model {
calculateTotalToPay(); calculateTotalToPay();
offer.resetState(); offer.resetState();
priceFeedService.setCurrencyCode(offer.getCurrencyCode()); priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode());
} }
@Override @Override
@ -147,7 +147,7 @@ public class TakeOfferModel implements Model {
private long getMaxTradeLimit() { private long getMaxTradeLimit() {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getCounterCurrencyCode(),
offer.getMirroredDirection(), offer.getMirroredDirection(),
offer.hasBuyerAsTakerWithoutDeposit()); offer.hasBuyerAsTakerWithoutDeposit());
} }

View file

@ -19,6 +19,7 @@ package haveno.core.payment;
import haveno.core.api.model.PaymentAccountFormField; import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.BankUtil;
import haveno.core.locale.TradeCurrency; import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.AchTransferAccountPayload;
import haveno.core.payment.payload.BankAccountPayload; import haveno.core.payment.payload.BankAccountPayload;
@ -34,6 +35,19 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.HOLDER_NAME,
PaymentAccountFormField.FieldId.HOLDER_ADDRESS,
PaymentAccountFormField.FieldId.BANK_NAME,
PaymentAccountFormField.FieldId.BRANCH_ID,
PaymentAccountFormField.FieldId.ACCOUNT_NR,
PaymentAccountFormField.FieldId.ACCOUNT_TYPE,
PaymentAccountFormField.FieldId.COUNTRY,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.SALT
);
public AchTransferAccount() { public AchTransferAccount() {
super(PaymentMethod.ACH_TRANSFER); super(PaymentMethod.ACH_TRANSFER);
} }
@ -79,6 +93,15 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme
@Override @Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
}
@Override
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
var field = super.getEmptyFormField(fieldId);
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
if (field.getId() == PaymentAccountFormField.FieldId.BRANCH_ID) field.setLabel(BankUtil.getBranchIdLabel("US"));
if (field.getId() == PaymentAccountFormField.FieldId.ACCOUNT_TYPE) field.setLabel(BankUtil.getAccountTypeLabel("US"));
return field;
} }
} }

View file

@ -60,6 +60,13 @@ public final class AliPayAccount extends PaymentAccount {
new TraditionalCurrency("ZAR") new TraditionalCurrency("ZAR")
); );
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.ACCOUNT_NR,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.SALT
);
public AliPayAccount() { public AliPayAccount() {
super(PaymentMethod.ALI_PAY); super(PaymentMethod.ALI_PAY);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -77,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount {
@Override @Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public void setAccountNr(String accountNr) { public void setAccountNr(String accountNr) {

View file

@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
new TraditionalCurrency("USD") new TraditionalCurrency("USD")
); );
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR,
PaymentAccountFormField.FieldId.COUNTRY,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.SALT
);
@Nullable @Nullable
private Country country; private Country country;
@ -65,7 +73,7 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
@Override @Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public String getEmailOrMobileNr() { public String getEmailOrMobileNr() {
@ -97,4 +105,11 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() { private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() {
return (AmazonGiftCardAccountPayload) paymentAccountPayload; return (AmazonGiftCardAccountPayload) paymentAccountPayload;
} }
@Override
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
var field = super.getEmptyFormField(fieldId);
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
return field;
}
} }

View file

@ -17,12 +17,15 @@
package haveno.core.payment; package haveno.core.payment;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.api.model.PaymentAccountFormField; import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency; import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.InteracETransferAccountPayload;
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.payment.validation.InteracETransferValidator;
import haveno.core.trade.HavenoUtils;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -33,6 +36,15 @@ public final class InteracETransferAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD"));
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.HOLDER_NAME,
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR,
PaymentAccountFormField.FieldId.QUESTION,
PaymentAccountFormField.FieldId.ANSWER,
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.SALT
);
public InteracETransferAccount() { public InteracETransferAccount() {
super(PaymentMethod.INTERAC_E_TRANSFER); super(PaymentMethod.INTERAC_E_TRANSFER);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,15 +62,15 @@ public final class InteracETransferAccount extends PaymentAccount {
@Override @Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public void setEmail(String email) { public void setEmail(String email) {
((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email); ((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email);
} }
public String getEmail() { public String getEmail() {
return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail(); return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr();
} }
public void setAnswer(String answer) { public void setAnswer(String answer) {
@ -84,4 +96,19 @@ public final class InteracETransferAccount extends PaymentAccount {
public String getHolderName() { public String getHolderName() {
return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName(); return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName();
} }
public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
InteracETransferValidator interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator;
switch (fieldId) {
case QUESTION:
processValidationResult(interacETransferValidator.questionValidator.validate(value));
break;
case ANSWER:
processValidationResult(interacETransferValidator.answerValidator.validate(value));
break;
default:
super.validateFormField(form, fieldId, value);
}
}
} }

View file

@ -18,8 +18,7 @@
package haveno.core.payment; package haveno.core.payment;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject; import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -47,13 +46,6 @@ import java.util.Map;
public class JapanBankData { public class JapanBankData {
private static String userLanguage;
@Inject
JapanBankData(Preferences preferences) {
userLanguage = preferences.getUserLanguage();
}
/* /*
Returns the main list of ~500 banks in Japan with bank codes, Returns the main list of ~500 banks in Japan with bank codes,
but since 90%+ of people will be using one of ~30 major banks, but since 90%+ of people will be using one of ~30 major banks,
@ -793,7 +785,7 @@ public class JapanBankData {
// don't localize these strings into all languages, // don't localize these strings into all languages,
// all we want is either Japanese or English here. // all we want is either Japanese or English here.
public static String getString(String id) { public static String getString(String id) {
boolean ja = userLanguage.equals("ja"); boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja");
switch (id) { switch (id) {
case "bank": case "bank":

View file

@ -435,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new LengthValidator(2, 100).validate(value)); processValidationResult(new LengthValidator(2, 100).validate(value));
break; break;
case ACCOUNT_TYPE: case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented"); processValidationResult(new LengthValidator(2, 100).validate(value));
break;
case ANSWER: case ANSWER:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case BANK_ACCOUNT_NAME: case BANK_ACCOUNT_NAME:
@ -491,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new BICValidator().validate(value)); processValidationResult(new BICValidator().validate(value));
break; break;
case BRANCH_ID: case BRANCH_ID:
throw new IllegalArgumentException("Not implemented"); processValidationResult(new LengthValidator(2, 34).validate(value));
break;
case CITY: case CITY:
processValidationResult(new LengthValidator(2, 34).validate(value)); processValidationResult(new LengthValidator(2, 34).validate(value));
break; break;
@ -518,7 +520,8 @@ public abstract class PaymentAccount implements PersistablePayload {
case EXTRA_INFO: case EXTRA_INFO:
break; break;
case HOLDER_ADDRESS: case HOLDER_ADDRESS:
throw new IllegalArgumentException("Not implemented"); processValidationResult(new LengthValidator(0, 100).validate(value));
break;
case HOLDER_EMAIL: case HOLDER_EMAIL:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME: case HOLDER_NAME:
@ -616,16 +619,20 @@ public abstract class PaymentAccount implements PersistablePayload {
break; break;
case ACCOUNT_NR: case ACCOUNT_NR:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel("payment.accountNr"); field.setLabel(Res.get("payment.accountNr"));
break; break;
case ACCOUNT_OWNER: case ACCOUNT_OWNER:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner")); field.setLabel(Res.get("payment.account.owner"));
break; break;
case ACCOUNT_TYPE: case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
field.setLabel(Res.get("payment.select.account"));
break;
case ANSWER: case ANSWER:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.answer"));
break;
case BANK_ACCOUNT_NAME: case BANK_ACCOUNT_NAME:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner")); field.setLabel(Res.get("payment.account.owner"));
@ -668,11 +675,11 @@ public abstract class PaymentAccount implements PersistablePayload {
break; break;
case BENEFICIARY_ACCOUNT_NR: case BENEFICIARY_ACCOUNT_NR:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.swift.account")); field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift
break; break;
case BENEFICIARY_ADDRESS: case BENEFICIARY_ADDRESS:
field.setComponent(PaymentAccountFormField.Component.TEXTAREA); field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
field.setLabel(Res.get("payment.swift.address.beneficiary")); field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift
break; break;
case BENEFICIARY_CITY: case BENEFICIARY_CITY:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
@ -691,7 +698,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel("BIC"); field.setLabel("BIC");
break; break;
case BRANCH_ID: case BRANCH_ID:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.TEXT);
//field.setLabel("Not implemented"); // expected to be overridden by subclasses
break;
case CITY: case CITY:
field.setComponent(PaymentAccountFormField.Component.TEXT); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.city")); field.setLabel(Res.get("payment.account.city"));
@ -717,7 +726,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.shared.optionalExtra")); field.setLabel(Res.get("payment.shared.optionalExtra"));
break; break;
case HOLDER_ADDRESS: case HOLDER_ADDRESS:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
field.setLabel(Res.get("payment.account.owner.address"));
break;
case HOLDER_EMAIL: case HOLDER_EMAIL:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME: case HOLDER_NAME:
@ -755,7 +766,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.swift.swiftCode.intermediary")); field.setLabel(Res.get("payment.swift.swiftCode.intermediary"));
break; break;
case MOBILE_NR: case MOBILE_NR:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.mobile"));
break;
case NATIONAL_ACCOUNT_ID: case NATIONAL_ACCOUNT_ID:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case PAYID: case PAYID:
@ -771,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload {
case PROMPT_PAY_ID: case PROMPT_PAY_ID:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case QUESTION: case QUESTION:
throw new IllegalArgumentException("Not implemented"); field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.secret"));
break;
case REQUIREMENTS: case REQUIREMENTS:
throw new IllegalArgumentException("Not implemented"); throw new IllegalArgumentException("Not implemented");
case SALT: case SALT:

View file

@ -50,6 +50,7 @@ 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.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.AMAZON_GIFT_CARD_ID;
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
@ -438,6 +439,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
// account.setSingleTradeCurrency(fiatCurrency); // 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 if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) {
((AmazonGiftCardAccount) account).setCountry(country.get());
} else { } else {
String errMsg = format("cannot set the country on a %s", String errMsg = format("cannot set the country on a %s",
paymentAccountType.getSimpleName()); paymentAccountType.getSimpleName());

View file

@ -122,9 +122,9 @@ public class PaymentAccountUtil {
public static boolean isAmountValidForOffer(Offer offer, public static boolean isAmountValidForOffer(Offer offer,
PaymentAccount paymentAccount, PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService) { AccountAgeWitnessService accountAgeWitnessService) {
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode());
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
return !hasChargebackRisk || hasValidAccountAgeWitness; return !hasChargebackRisk || hasValidAccountAgeWitness;
} }

View file

@ -95,7 +95,7 @@ class ReceiptPredicates {
.map(TradeCurrency::getCode) .map(TradeCurrency::getCode)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return codes.contains(offer.getCurrencyCode()); return codes.contains(offer.getCounterCurrencyCode());
} }
boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) { boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) {

View file

@ -17,12 +17,14 @@
package haveno.core.payment; package haveno.core.payment;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.api.model.PaymentAccountFormField; import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency; import haveno.core.locale.TradeCurrency;
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.payment.payload.SwishAccountPayload; import haveno.core.payment.payload.SwishAccountPayload;
import haveno.core.payment.validation.SwishValidator;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NonNull; import lombok.NonNull;
@ -33,6 +35,13 @@ public final class SwishAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK"));
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.MOBILE_NR,
PaymentAccountFormField.FieldId.HOLDER_NAME,
PaymentAccountFormField.FieldId.SALT
);
public SwishAccount() { public SwishAccount() {
super(PaymentMethod.SWISH); super(PaymentMethod.SWISH);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,7 +59,7 @@ public final class SwishAccount extends PaymentAccount {
@Override @Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public void setMobileNr(String mobileNr) { public void setMobileNr(String mobileNr) {
@ -68,4 +77,16 @@ public final class SwishAccount extends PaymentAccount {
public String getHolderName() { public String getHolderName() {
return ((SwishAccountPayload) paymentAccountPayload).getHolderName(); return ((SwishAccountPayload) paymentAccountPayload).getHolderName();
} }
@Override
public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
switch (fieldId) {
case MOBILE_NR:
processValidationResult(new SwishValidator().validate(value));
break;
default:
super.validateFormField(form, fieldId, value);
break;
}
}
} }

View file

@ -19,6 +19,7 @@ package haveno.core.payment;
import haveno.core.api.model.PaymentAccountFormField; import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TraditionalCurrency;
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;
import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.PaymentMethod;
@ -33,6 +34,15 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.EMAIL,
PaymentAccountFormField.FieldId.HOLDER_NAME,
PaymentAccountFormField.FieldId.HOLDER_ADDRESS,
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.COUNTRY,
PaymentAccountFormField.FieldId.SALT
);
public TransferwiseUsdAccount() { public TransferwiseUsdAccount() {
super(PaymentMethod.TRANSFERWISE_USD); super(PaymentMethod.TRANSFERWISE_USD);
// this payment method is currently restricted to United States/USD // this payment method is currently restricted to United States/USD
@ -61,11 +71,11 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
} }
public void setBeneficiaryAddress(String address) { public void setBeneficiaryAddress(String address) {
((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address); ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address);
} }
public String getBeneficiaryAddress() { public String getBeneficiaryAddress() {
return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress();
} }
@Override @Override
@ -90,6 +100,13 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
@Override @Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
}
@Override
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
var field = super.getEmptyFormField(fieldId);
if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_ADDRESS) field.setLabel(field.getLabel() + " " + Res.get("payment.transferwiseUsd.address"));
return field;
} }
} }

View file

@ -33,6 +33,13 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.HOLDER_NAME,
PaymentAccountFormField.FieldId.POSTAL_ADDRESS,
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.SALT
);
public USPostalMoneyOrderAccount() { public USPostalMoneyOrderAccount() {
super(PaymentMethod.US_POSTAL_MONEY_ORDER); super(PaymentMethod.US_POSTAL_MONEY_ORDER);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,7 +57,7 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount {
@Override @Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public void setPostalAddress(String postalAddress) { public void setPostalAddress(String postalAddress) {

View file

@ -38,6 +38,13 @@ public final class WeChatPayAccount extends PaymentAccount {
new TraditionalCurrency("GBP") new TraditionalCurrency("GBP")
); );
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.ACCOUNT_NR,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.SALT
);
public WeChatPayAccount() { public WeChatPayAccount() {
super(PaymentMethod.WECHAT_PAY); super(PaymentMethod.WECHAT_PAY);
} }
@ -54,7 +61,7 @@ public final class WeChatPayAccount extends PaymentAccount {
@Override @Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() { public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented"); return INPUT_FIELD_IDS;
} }
public void setAccountNr(String accountNr) { public void setAccountNr(String accountNr) {

View file

@ -36,7 +36,7 @@ import java.util.Map;
@Getter @Getter
@Slf4j @Slf4j
public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String email = ""; private String emailOrMobileNr = "";
private String holderName = ""; private String holderName = "";
private String question = ""; private String question = "";
private String answer = ""; private String answer = "";
@ -52,7 +52,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
private InteracETransferAccountPayload(String paymentMethod, private InteracETransferAccountPayload(String paymentMethod,
String id, String id,
String email, String emailOrMobileNr,
String holderName, String holderName,
String question, String question,
String answer, String answer,
@ -62,7 +62,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
id, id,
maxTradePeriod, maxTradePeriod,
excludeFromJsonDataMap); excludeFromJsonDataMap);
this.email = email; this.emailOrMobileNr = emailOrMobileNr;
this.holderName = holderName; this.holderName = holderName;
this.question = question; this.question = question;
this.answer = answer; this.answer = answer;
@ -72,7 +72,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
public Message toProtoMessage() { public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder() return getPaymentAccountPayloadBuilder()
.setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder() .setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder()
.setEmail(email) .setEmailOrMobileNr(emailOrMobileNr)
.setHolderName(holderName) .setHolderName(holderName)
.setQuestion(question) .setQuestion(question)
.setAnswer(answer)) .setAnswer(answer))
@ -82,7 +82,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
return new InteracETransferAccountPayload(proto.getPaymentMethodId(), return new InteracETransferAccountPayload(proto.getPaymentMethodId(),
proto.getId(), proto.getId(),
proto.getInteracETransferAccountPayload().getEmail(), proto.getInteracETransferAccountPayload().getEmailOrMobileNr(),
proto.getInteracETransferAccountPayload().getHolderName(), proto.getInteracETransferAccountPayload().getHolderName(),
proto.getInteracETransferAccountPayload().getQuestion(), proto.getInteracETransferAccountPayload().getQuestion(),
proto.getInteracETransferAccountPayload().getAnswer(), proto.getInteracETransferAccountPayload().getAnswer(),
@ -98,21 +98,21 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
@Override @Override
public String getPaymentDetails() { public String getPaymentDetails() {
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " +
Res.get("payment.email") + " " + email + ", " + Res.getWithCol("payment.secret") + " " + Res.get("payment.email") + " " + emailOrMobileNr + ", " + Res.getWithCol("payment.secret") + " " +
question + ", " + Res.getWithCol("payment.answer") + " " + answer; question + ", " + Res.getWithCol("payment.answer") + " " + answer;
} }
@Override @Override
public String getPaymentDetailsForTradePopup() { public String getPaymentDetailsForTradePopup() {
return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" +
Res.getWithCol("payment.email") + " " + email + "\n" + Res.getWithCol("payment.email") + " " + emailOrMobileNr + "\n" +
Res.getWithCol("payment.secret") + " " + question + "\n" + Res.getWithCol("payment.secret") + " " + question + "\n" +
Res.getWithCol("payment.answer") + " " + answer; Res.getWithCol("payment.answer") + " " + answer;
} }
@Override @Override
public byte[] getAgeWitnessInputData() { public byte[] getAgeWitnessInputData() {
return super.getAgeWitnessInputData(ArrayUtils.addAll(email.getBytes(StandardCharsets.UTF_8), return super.getAgeWitnessInputData(ArrayUtils.addAll(emailOrMobileNr.getBytes(StandardCharsets.UTF_8),
ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8), ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8),
answer.getBytes(StandardCharsets.UTF_8)))); answer.getBytes(StandardCharsets.UTF_8))));
} }

View file

@ -369,7 +369,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
CASH_APP_ID, CASH_APP_ID,
PAYPAL_ID, PAYPAL_ID,
VENMO_ID, VENMO_ID,
PAYSAFE_ID); PAYSAFE_ID,
WECHAT_PAY_ID,
ALI_PAY_ID,
SWISH_ID,
TRANSFERWISE_USD_ID,
AMAZON_GIFT_CARD_ID,
ACH_TRANSFER_ID,
INTERAC_E_TRANSFER_ID,
US_POSTAL_MONEY_ORDER_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());
} }

View file

@ -39,7 +39,7 @@ import java.util.Map;
public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload { public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload {
private String email = ""; private String email = "";
private String holderName = ""; private String holderName = "";
private String beneficiaryAddress = ""; private String holderAddress = "";
public TransferwiseUsdAccountPayload(String paymentMethod, String id) { public TransferwiseUsdAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id); super(paymentMethod, id);
@ -51,7 +51,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
List<String> acceptedCountryCodes, List<String> acceptedCountryCodes,
String email, String email,
String holderName, String holderName,
String beneficiaryAddress, String holderAddress,
long maxTradePeriod, long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) { Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod, super(paymentMethod,
@ -63,7 +63,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
this.email = email; this.email = email;
this.holderName = holderName; this.holderName = holderName;
this.beneficiaryAddress = beneficiaryAddress; this.holderAddress = holderAddress;
} }
@Override @Override
@ -71,7 +71,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder() protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder()
.setEmail(email) .setEmail(email)
.setHolderName(holderName) .setHolderName(holderName)
.setBeneficiaryAddress(beneficiaryAddress); .setHolderAddress(holderAddress);
final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder()
.getCountryBasedPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder()
.setTransferwiseUsdAccountPayload(builder); .setTransferwiseUsdAccountPayload(builder);
@ -89,7 +89,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()),
accountPayloadPB.getEmail(), accountPayloadPB.getEmail(),
accountPayloadPB.getHolderName(), accountPayloadPB.getHolderName(),
accountPayloadPB.getBeneficiaryAddress(), accountPayloadPB.getHolderAddress(),
proto.getMaxTradePeriod(), proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap())); new HashMap<>(proto.getExcludeFromJsonDataMap()));
} }

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.getMinSecurityDepositAsPercent(); double minPercentage = Restrictions.getMinSecurityDepositPct();
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.getMaxSecurityDepositAsPercent(); double maxPercentage = Restrictions.getMaxSecurityDepositPct();
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

@ -53,7 +53,7 @@ public class ProvidersRepository {
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 "http://agorise7ae5g7lkqp7r7qddsyzskft7cqhgguwkadbqamtsrap5onead.onion/" // Agorise
); );
private final Config config; private final Config config;

View file

@ -296,13 +296,13 @@ public class PriceFeedService {
} }
} }
private void setHavenoMarketPrice(String currencyCode, Price price) { private void setHavenoMarketPrice(String counterCurrencyCode, Price price) {
UserThread.execute(() -> { UserThread.execute(() -> {
String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode); String counterCurrencyCodeBase = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode);
synchronized (cache) { synchronized (cache) {
if (!cache.containsKey(currencyCodeBase) || !cache.get(currencyCodeBase).isExternallyProvidedPrice()) { if (!cache.containsKey(counterCurrencyCodeBase) || !cache.get(counterCurrencyCodeBase).isExternallyProvidedPrice()) {
cache.put(currencyCodeBase, new MarketPrice(currencyCodeBase, cache.put(counterCurrencyCodeBase, new MarketPrice(counterCurrencyCodeBase,
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT), MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(counterCurrencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
0, 0,
false)); false));
} }
@ -371,9 +371,7 @@ public class PriceFeedService {
} }
/** /**
* Returns prices for all available currencies. * Returns prices for all available currencies. The base currency is always XMR.
* For crypto currencies the value is XMR price for 1 unit of given crypto currency (e.g. 1 DOGE = X XMR).
* For traditional currencies the value is price in the given traditional currency per 1 XMR (e.g. 1 XMR = X USD).
* *
* TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout * TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout
*/ */

View file

@ -28,6 +28,8 @@ import haveno.network.p2p.P2PService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -63,16 +65,20 @@ public class PriceProvider extends HttpClientProvider {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj; LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode"); String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode");
String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode"); String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode");
String currencyCode = baseCurrencyCode.equals("XMR") ? counterCurrencyCode : baseCurrencyCode; boolean isInverted = !"XMR".equalsIgnoreCase(baseCurrencyCode);
currencyCode = CurrencyUtil.getCurrencyCodeBase(currencyCode); if (isInverted) {
String temp = baseCurrencyCode;
baseCurrencyCode = counterCurrencyCode;
counterCurrencyCode = temp;
}
counterCurrencyCode = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode);
double price = (Double) treeMap.get("price"); double price = (Double) treeMap.get("price");
// json uses double for our timestampSec long value... if (isInverted) price = BigDecimal.ONE.divide(BigDecimal.valueOf(price), 10, RoundingMode.HALF_UP).doubleValue(); // XMR is always base currency, so invert price if applicable
long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); marketPriceMap.put(counterCurrencyCode, new MarketPrice(counterCurrencyCode, price, timestampSec, true));
} catch (Throwable t) { } catch (Throwable t) {
log.error("Error getting all prices: {}\n", t.getMessage(), t); log.error("Error getting all prices: {}\n", t.getMessage(), t);
} }
}); });
return marketPriceMap; return marketPriceMap;
} }

View file

@ -154,15 +154,15 @@ public abstract class SupportManager {
// Message handler // Message handler
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
protected void handleChatMessage(ChatMessage chatMessage) { protected void handle(ChatMessage chatMessage) {
final String tradeId = chatMessage.getTradeId(); final String tradeId = chatMessage.getTradeId();
final String uid = chatMessage.getUid(); final String uid = chatMessage.getUid();
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid); log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
boolean channelOpen = channelOpen(chatMessage); boolean channelOpen = channelOpen(chatMessage);
if (!channelOpen) { if (!channelOpen) {
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); log.warn("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1); Timer timer = UserThread.runAfter(() -> handle(chatMessage), 1);
delayMsgMap.put(uid, timer); delayMsgMap.put(uid, timer);
} else { } else {
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
@ -217,7 +217,11 @@ public abstract class SupportManager {
synchronized (dispute.getChatMessages()) { synchronized (dispute.getChatMessages()) {
for (ChatMessage chatMessage : dispute.getChatMessages()) { for (ChatMessage chatMessage : dispute.getChatMessages()) {
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) { if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
if (trade.getDisputeState().isCloseRequested()) { if (trade.getDisputeState().isRequested()) {
log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
dispute.setIsClosed();
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
} else if (trade.getDisputeState().isCloseRequested()) {
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress()); log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
dispute.setIsClosed(); dispute.setIsClosed();
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);

View file

@ -392,7 +392,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
change += "chat messages;"; change += "chat messages;";
} }
if (change.length() > 0) { if (change.length() > 0) {
log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); log.info("Cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId()));
} }
} }

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