Compare commits

...

270 commits

Author SHA1 Message Date
woodser
a7ae00858d
decrease DisputeAgent TTL to 7 days (#2012) 2025-10-16 08:53:13 -04:00
woodser
231d8a6615
update to monero-java v0.8.39, set proxy with daemon connection (#2019) 2025-10-15 07:46:36 -04:00
woodser
ea057ada4d
update to monero-project v0.18.4.3 (#2018) 2025-10-14 09:41:03 -04:00
woodser
c1f2ee9e77
fix offer message order by waiting 5s after edit offer start to publish (#2016) 2025-10-11 15:20:03 -04:00
woodser
ed41671df9
resend dispute opened message until received (#2008) 2025-10-11 09:20:08 -04:00
woodser
dd3fe5b09e
reprocess offer on republish if arbitrator signature is missing (#2010) 2025-10-11 09:19:13 -04:00
woodser
e592ad557d
prevent removing triggered offer from network on edit (#2015) 2025-10-11 09:18:49 -04:00
woodser
39218c1708
support editing offers via grpc api (#2014) 2025-10-11 09:18:40 -04:00
PromptPunksFauxCough
c07408563a
Update freedesktop rt version in exchange.haveno.Haveno.yml (#2013) 2025-10-09 19:13:43 -04:00
woodser
0feb559884
fix navigating to open offers on close button of offer created popup (#2009) 2025-10-09 08:04:27 -04:00
woodser
cff4e5ea6c update docs to avoid restarting seed nodes at the same time 2025-10-09 07:28:46 -04:00
woodser
5ed6ab1fec update docs link to clone offers 2025-10-09 07:28:46 -04:00
woodser
659e130329
set min trade amount for account signing to min trade amount (0.05 xmr) (#2003) 2025-10-09 07:28:20 -04:00
woodser
e882563e3c save wallet after syncing before stopping timeout 2025-10-09 07:28:09 -04:00
woodser
ee35e34eaa disable repeat sync to latest target height after first sync 2025-10-09 07:28:09 -04:00
woodser
f21bb3a3ed
support activating and deactivating offers over grpc api (#2001) 2025-10-05 00:50:03 -04:00
woodser
5a5e1653c0
update hyperlinks after docs refactor (#1999) 2025-10-02 15:54:54 -04:00
woodser
8424a65328
fix log spam on startup when shutting down past trades (#1998) 2025-10-01 01:02:29 -04:00
woodser
1abbe83005
remove chat warning when missing dispute trade (#1997) 2025-09-30 06:31:53 -04:00
woodser
6ccb95e2eb reset reserve tx on error creating reserve tx 2025-09-30 06:19:30 -04:00
woodser
34050f6b9f edit open offer on shared thread to fix concurrent processing 2025-09-30 06:19:30 -04:00
woodser
5c42536d6d
fix log message when sending dispute opened message (#1994) 2025-09-30 00:21:36 -04:00
woodser
fbadb03ab7
wallet poll looper runs off thread so polls do not build up (#1993) 2025-09-30 00:21:25 -04:00
woodser
17afbca679
bump version to v1.2.2 (#1995) 2025-09-29 13:20:51 -04:00
woodser
aefa803971
fix reinitializing trade when moved from failed back to pending (#1991) 2025-09-29 09:40:07 -04:00
woodser
f252f0b40c automatically close disputes on startup when payout is published 2025-09-29 09:18:58 -04:00
woodser
eca5ec6e8f do not set dispute result on cancel 2025-09-29 09:18:58 -04:00
woodser
f0f451c13d disable dispute summary text area and close button after closed and published 2025-09-29 09:18:58 -04:00
woodser
f56831eb10 fix npe showing dispute result of published trade 2025-09-29 09:18:58 -04:00
woodser
bd12a1a4e6 fail preparing payment sent message if payout already published 2025-09-29 01:12:16 -04:00
woodser
57d9701ef0 request save main wallet after elapsed time on poll 2025-09-29 01:11:48 -04:00
woodser
96e0e78bf3 assign stopped state in use standby mode listener 2025-09-29 01:10:54 -04:00
woodser
60ee90f53b avoid blocking by checking if wallet exists without lock 2025-09-27 12:48:12 -04:00
woodser
1a3bee0300 fix message spam by resending payment received messages when applicable 2025-09-27 12:48:12 -04:00
woodser
6214f4a120
skip reverting deposit or payout tx state until next confirmation (#1984) 2025-09-27 00:46:58 -04:00
woodser
a7c6e15f2e
decrease resend period for payment received message to 1 month (#1986) 2025-09-27 00:46:34 -04:00
woodser
f8135ad134
decrease sync timeout to 3 minutes (#1985) 2025-09-27 00:46:25 -04:00
woodser
527663d50b
only add system message once when dispute opened (#1983)
add system message only once when dispute is opened
2025-09-27 00:40:44 -04:00
woodser
5b35884d3e
skip trade shutdown log when finalized in previous release (#1982) 2025-09-27 00:38:27 -04:00
woodser
457518e3a9 increase max popup height to 850 2025-09-26 06:45:10 -04:00
woodser
04a8876da5 do not cut off offer agreement terms and conditions 2025-09-26 06:45:10 -04:00
woodser
545cc7bffc set fixed height for copy passphrase button 2025-09-26 06:45:10 -04:00
woodser
4ca05692dd
support moving trades to failed trades with right click (#1978) 2025-09-26 00:03:14 -04:00
woodser
518ebaff88
fix syncing wallets over tor on first sync (#1977) 2025-09-26 00:02:58 -04:00
woodser
5f7b55d061
save wallets after elapsed time and on wallet operations (#1975) 2025-09-24 03:45:30 -04:00
woodser
bc2ce1e77d
fix spacing of text field icon in buyer step view (#1976) 2025-09-24 03:31:56 -04:00
woodser
fb361730c7 use nack flow if cannot create payout tx and stop repeat sending 2025-09-24 01:39:34 -04:00
woodser
9694db4643 skip updating states if no change 2025-09-24 01:39:34 -04:00
woodser
cb0864699b shut down trade when moved to failed 2025-09-24 01:39:34 -04:00
woodser
197f6b086a only resync for missing deposit txs if state is seen 2025-09-24 01:39:34 -04:00
woodser
d7c934eac4 increase app startup timeout to 5 mins 2025-09-23 01:53:40 -04:00
woodser
7ccd4bed8e increase wallet sync timeout to 4 mins 2025-09-23 01:53:40 -04:00
woodser
d9954215b1 fixes to update multisig until finalized and delete wallet on trade thread 2025-09-23 01:53:40 -04:00
woodser
fee4bd0202 save dispute closed message for reprocessing immediately 2025-09-22 07:32:24 -04:00
woodser
ca6ef3dc9a fix trade wallet syncing before deposits requested 2025-09-22 07:32:24 -04:00
woodser
06f472dc53 refactor dispute preparation and requesting off main thread 2025-09-22 07:32:24 -04:00
woodser
4e188a9343 refactor polling to recover if trade tx not fetched 2025-09-22 07:32:24 -04:00
woodser
9c4573487a
reduce memory consumption to get encrypted payloads with hmac (#1971) 2025-09-22 07:32:13 -04:00
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
413 changed files with 11226 additions and 5660 deletions

View file

@ -14,7 +14,18 @@ jobs:
build:
strategy:
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
runs-on: ${{ matrix.os }}
steps:
@ -27,8 +38,25 @@ jobs:
java-version: '21'
distribution: 'adopt'
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
- 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
if: failure()
with:
@ -38,115 +66,131 @@ jobs:
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: cached-localnet
name: cached-localnet-${{ matrix.os }}
path: .localnet
overwrite: true
- name: Install dependencies
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Install WiX Toolset
if: ${{ matrix.os == 'windows-latest' }}
if: runner.os == 'Windows'
run: |
Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe
.\wix314.exe /quiet /norestart
shell: powershell
- name: Build Haveno Installer
- 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: |
./gradlew clean build --refresh-keys --refresh-dependencies
./gradlew packageInstallers
./gradlew clean build --refresh-keys --refresh-dependencies -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."
working-directory: .
- name: Package Haveno Installer
run: ./gradlew packageInstallers
working-directory: .
# get version from jar
- name: Set Version Unix
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
if: runner.os != 'Windows'
run: |
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Set Version Windows
if: ${{ matrix.os == 'windows-latest' }}
if: runner.os == 'Windows'
run: |
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
shell: powershell
- name: Move Release Files for Linux
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
run: |
mkdir ${{ github.workspace }}/release-linux-rpm
mkdir ${{ github.workspace }}/release-linux-deb
mkdir ${{ github.workspace }}/release-linux-flatpak
mkdir ${{ github.workspace }}/release-linux-appimage
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
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-${{ matrix.arch }}-installer.deb
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-${{ 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-rpm
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
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
- name: Move Release Files for macOS
if: ${{ matrix.os == 'macos-13' }}
if: runner.os == 'MacOS'
run: |
mkdir ${{ github.workspace }}/release-macos
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
mkdir ${{ github.workspace }}/release-macos-${{ matrix.arch }}
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-${{ matrix.arch }}
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
- name: Move Release Files on Windows
if: ${{ matrix.os == 'windows-latest' }}
if: runner.os == 'Windows'
run: |
mkdir ${{ github.workspace }}/release-windows
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
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
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
shell: powershell
# win
# Windows artifacts
- uses: actions/upload-artifact@v4
name: "Windows artifacts"
if: ${{ matrix.os == 'windows-latest' }}
if: runner.os == 'Windows'
with:
name: haveno-windows
name: haveno-windows-${{ matrix.arch }}
path: ${{ github.workspace }}/release-windows
# macos
# macOS artifacts
- uses: actions/upload-artifact@v4
name: "macOS artifacts"
if: ${{ matrix.os == 'macos-13' }}
if: runner.os == 'MacOS'
with:
name: haveno-macos
path: ${{ github.workspace }}/release-macos
# linux
name: haveno-macos-${{ matrix.arch }}
path: ${{ github.workspace }}/release-macos-${{ matrix.arch }}
# Linux artifacts
- uses: actions/upload-artifact@v4
name: "Linux - deb artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
with:
name: haveno-linux-deb
name: haveno-linux-${{ matrix.arch }}-deb
path: ${{ github.workspace }}/release-linux-deb
- uses: actions/upload-artifact@v4
name: "Linux - rpm artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
with:
name: haveno-linux-rpm
name: haveno-linux-${{ matrix.arch }}-rpm
path: ${{ github.workspace }}/release-linux-rpm
- uses: actions/upload-artifact@v4
name: "Linux - AppImage artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
with:
name: haveno-linux-appimage
name: haveno-linux-${{ matrix.arch }}-appimage
path: ${{ github.workspace }}/release-linux-appimage
- uses: actions/upload-artifact@v4
name: "Linux - flatpak artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: runner.os == 'Linux'
with:
name: haveno-linux-flatpak
name: haveno-linux-${{ matrix.arch }}-flatpak
path: ${{ github.workspace }}/release-linux-flatpak
- name: Release
@ -154,14 +198,30 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
# Linux x86_64
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
# Linux aarch64
${{ 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
# https://git-scm.com/docs/git-tag - git-tag Docu

View file

@ -9,7 +9,7 @@ jobs:
build:
if: github.repository == 'haveno-dex/haveno'
name: Publish coverage
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@ -19,6 +19,11 @@ jobs:
java-version: '21'
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
run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar

View file

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

View file

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

View file

@ -70,11 +70,12 @@ monerod1-local:
--log-level 0 \
--add-exclusive-node 127.0.0.1:48080 \
--add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod2-local:
./.localnet/monerod \
@ -90,11 +91,12 @@ monerod2-local:
--confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod3-local:
./.localnet/monerod \
@ -110,11 +112,12 @@ monerod3-local:
--confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:48080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--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 \
@ -440,6 +443,9 @@ monerod:
./.localnet/monerod \
--bootstrap-daemon-address auto \
--rpc-access-control-origins http://localhost:8080 \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
seednode:
./haveno-seednode$(APP_EXT) \
@ -485,6 +491,31 @@ arbitrator-desktop-mainnet:
--xmrNode=http://127.0.0.1:18081 \
--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$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
@ -570,3 +601,19 @@ user3-desktop-mainnet:
--apiPort=1204 \
--useNativeXmrWallet=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">
<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)
[![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)
@ -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.
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
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>
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
<code>47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB</code>
</p>
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.

View file

@ -43,7 +43,7 @@ import java.util.stream.Collectors;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
@ -158,7 +158,7 @@ public class MethodTest extends ApiTestCase {
}
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct());
if (defaultPct.precision() != 2)
throw new IllegalStateException(format(
"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() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (ERC20)", "USDT-ERC20");
super("Tether USD", "USDT-ERC20");
}
}

View file

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

View file

@ -22,6 +22,6 @@ import haveno.asset.Erc20Token;
public class USDCoinERC20 extends Erc20Token {
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.
haveno.asset.coins.Bitcoin$Mainnet
haveno.asset.coins.BitcoinCash
haveno.asset.coins.Cardano
haveno.asset.coins.Dogecoin
haveno.asset.coins.Ether
haveno.asset.coins.Litecoin
haveno.asset.coins.Monero
haveno.asset.coins.Ripple
haveno.asset.coins.Solana
haveno.asset.coins.Tron
haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20
haveno.asset.tokens.USDCoinERC20

View file

@ -4,11 +4,6 @@
# E.g.: [main-view].[component].[description]
# 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
# 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

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'
guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0'
moneroJavaVersion = '0.8.36'
moneroJavaVersion = '0.8.39'
httpclient5Version = '5.0'
hamcrestVersion = '2.2'
httpclientVersion = '4.5.12'
@ -79,7 +79,9 @@ configure(subprojects) {
slf4jVersion = '1.7.30'
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 {
@ -457,14 +459,14 @@ configure(project(':core')) {
doLast {
// get monero binaries download url
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-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release8/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : '17b1bd82a4e0fa51fbf0ddab2f0639455c0e733d15e16e14865bd3fb7edbeca9',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release8/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : '759f210de6f3358779b96841b4de16622d2b70d8bc813941c8a421ea34392abe',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release8/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : '1a56ed4ee78f0ce463814c9dd34b52f8096b78a913dcfb18c4d4f717358dd778',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release8/monero-bins-haveno-windows.zip',
'windows-sha256' : '65b0e22841e58e8c935d358cc0543b58d675393136397a2e136aeb661b461e7e'
]
String osKey
@ -610,7 +612,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.19-SNAPSHOT'
version = '1.2.2-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

View file

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.19";
public static final String VERSION = "1.2.2";
/**
* 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.
// 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.
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// 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;
public static String getP2PMessageVersion() {

View file

@ -119,6 +119,7 @@ public class Config {
public static final String PASSWORD_REQUIRED = "passwordRequired";
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
public static final String DISABLE_RATE_LIMITS = "disableRateLimits";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -208,6 +209,7 @@ public class Config {
public final boolean passwordRequired;
public final boolean updateXmrBinaries;
public final String xmrBlockchainPath;
public final boolean disableRateLimits;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@ -639,6 +641,13 @@ public class Config {
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> disableRateLimits =
parser.accepts(DISABLE_RATE_LIMITS,
"Disables all API rate limits")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -753,6 +762,7 @@ public class Config {
this.passwordRequired = options.valueOf(passwordRequiredOpt);
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
this.disableRateLimits = options.valueOf(disableRateLimits);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),

View file

@ -20,7 +20,6 @@ package haveno.common.crypto;
import haveno.common.util.Hex;
import haveno.common.util.Utilities;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
@ -101,36 +100,19 @@ public class Encryption {
///////////////////////////////////////////////////////////////////////////////////////////
private static byte[] getPayloadWithHmac(byte[] payload, SecretKey secretKey) {
byte[] payloadWithHmac;
try {
ByteArrayOutputStream outputStream = null;
try {
byte[] hmac = getHmac(payload, secretKey);
outputStream = new ByteArrayOutputStream();
byte[] hmac = getHmac(payload, secretKey);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(payload.length + hmac.length)) {
outputStream.write(payload);
outputStream.write(hmac);
outputStream.flush();
payloadWithHmac = outputStream.toByteArray().clone();
} catch (IOException | NoSuchProviderException e) {
log.error("Could not create hmac", e);
throw new RuntimeException("Could not create hmac");
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignored) {
}
}
return outputStream.toByteArray();
}
} catch (Throwable e) {
log.error("Could not create hmac", e);
throw new RuntimeException("Could not create hmac");
throw new RuntimeException("Could not create hmac", e);
}
return payloadWithHmac;
}
private static boolean verifyHmac(byte[] message, byte[] hmac, SecretKey secretKey) {
try {
byte[] hmacTest = getHmac(message, secretKey);

View file

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

View file

@ -22,6 +22,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.Deque;
@ -85,6 +86,11 @@ public class MathUtils {
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) {
double factor = Math.pow(10, exponent);
return value / factor;
@ -95,6 +101,11 @@ public class MathUtils {
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) {
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
}

View file

@ -29,8 +29,8 @@ import haveno.common.util.Utilities;
import haveno.core.account.witness.AccountAgeWitness;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.user.User;
import haveno.core.xmr.wallet.Restrictions;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.P2PDataStorage;
@ -60,7 +60,7 @@ import org.bitcoinj.core.Utils;
public class SignedWitnessService {
public static final long SIGNER_AGE_DAYS = 30;
private static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis();
public static final BigInteger MINIMUM_TRADE_AMOUNT_FOR_SIGNING = HavenoUtils.xmrToAtomicUnits(.1);
public static final BigInteger MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Restrictions.getMinTradeAmount();
private final KeyRing keyRing;
private final P2PService p2PService;
@ -335,12 +335,13 @@ public class SignedWitnessService {
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
key.verifyMessage(message, signatureBase64);
verifySignatureWithECKeyResultCache.put(hash, true);
return true;
} else {
log.warn("Provided EC key is not in list of valid arbitrators.");
log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
verifySignatureWithECKeyResultCache.put(hash, false);
return false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -49,9 +49,7 @@ import haveno.core.api.model.MarketPriceInfo;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.app.AppStartupState;
import haveno.core.monetary.Price;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.PaymentMethod;
@ -66,7 +64,6 @@ import haveno.core.xmr.XmrNodeSettings;
import haveno.proto.grpc.NotificationMessage;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@ -299,8 +296,12 @@ public class CoreApi {
return walletsService.createXmrTx(destinations);
}
public String relayXmrTx(String metadata) {
return walletsService.relayXmrTx(metadata);
public List<MoneroTxWallet> createXmrSweepTxs(String address) {
return walletsService.createXmrSweepTxs(address);
}
public List<String> relayXmrTxs(List<String> metadatas) {
return walletsService.relayXmrTxs(metadatas);
}
public long getAddressBalance(String addressString) {
@ -448,32 +449,34 @@ public class CoreApi {
errorMessageHandler);
}
public Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
Price price,
boolean useMarketBasedPrice,
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return coreOffersService.editOffer(offerId,
public void editOffer(String offerId,
String currencyCode,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMarginPct,
String triggerPriceAsString,
String paymentAccountId,
String extraInfo,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreOffersService.editOffer(offerId,
currencyCode,
direction,
price,
priceAsString,
useMarketBasedPrice,
marketPriceMargin,
amount,
minAmount,
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
marketPriceMarginPct,
triggerPriceAsString,
paymentAccountId,
extraInfo,
resultHandler,
errorMessageHandler);
}
public void deactivateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
coreOffersService.deactivateOffer(offerId, resultHandler, errorMessageHandler);
}
public void activateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
coreOffersService.activateOffer(offerId, resultHandler, errorMessageHandler);
}
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {

View file

@ -22,7 +22,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.FaultHandler;
@ -101,57 +100,51 @@ public class CoreDisputesService {
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
Trade trade = tradeManager.getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
Offer offer = trade.getOffer();
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
// open dispute on trade thread
ThreadUtils.execute(() -> {
Offer offer = trade.getOffer();
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
var disputeManager = arbitrationManager;
var isSupportTicket = false;
var isMaker = tradeManager.isMyOffer(offer);
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
var disputeManager = arbitrationManager;
var isSupportTicket = false;
var isMaker = tradeManager.isMyOffer(offer);
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
tradeManager.requestPersistence();
}, trade.getId());
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
tradeManager.requestPersistence();
}
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
synchronized (trade.getLock()) {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing();
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
Dispute dispute = new Dispute(new Date().getTime(),
trade.getId(),
pubKey.hashCode(), // trader id,
true,
(offer.getDirection() == OfferDirection.BUY) == isMaker,
isMaker,
pubKey,
trade.getDate().getTime(),
trade.getMaxTradePeriodDate().getTime(),
trade.getContract(),
trade.getContractHash(),
payoutTxSerialized,
payoutTxHashAsString,
trade.getContractAsJson(),
trade.getMaker().getContractSignature(),
trade.getTaker().getContractSignature(),
trade.getMaker().getPaymentAccountPayload(),
trade.getTaker().getPaymentAccountPayload(),
arbitratorPubKeyRing,
isSupportTicket,
SupportType.ARBITRATION);
PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing();
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
Dispute dispute = new Dispute(new Date().getTime(),
trade.getId(),
pubKey.hashCode(), // trader id,
true,
(offer.getDirection() == OfferDirection.BUY) == isMaker,
isMaker,
pubKey,
trade.getDate().getTime(),
trade.getMaxTradePeriodDate().getTime(),
trade.getContract(),
trade.getContractHash(),
payoutTxSerialized,
payoutTxHashAsString,
trade.getContractAsJson(),
trade.getMaker().getContractSignature(),
trade.getTaker().getContractSignature(),
trade.getMaker().getPaymentAccountPayload(),
trade.getTaker().getPaymentAccountPayload(),
arbitratorPubKeyRing,
isSupportTicket,
SupportType.ARBITRATION);
return dispute;
}
return dispute;
}
// TODO: does not wait for success or error response
@ -241,14 +234,26 @@ public class CoreDisputesService {
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
if (!trade.isPayoutPublished() && 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) {
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
if (!trade.isPayoutPublished() && 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) {
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
if (!trade.isPayoutPublished() && customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.valueOf(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? customWinnerAmount : loserAmount));
@ -301,6 +306,7 @@ public class CoreDisputesService {
Dispute dispute;
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute with id '%s' not found", disputeId));
if (!arbitrationManager.canSendChatMessages(dispute)) throw new IllegalStateException(format("dispute with id '%s' cannot send chat messages (must be open or stored to mailbox)", disputeId));
ChatMessage chatMessage = new ChatMessage(
arbitrationManager.getSupportType(),
dispute.getTradeId(),

View file

@ -44,6 +44,7 @@ import static haveno.common.util.MathUtils.roundDoubleToLong;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.monetary.CryptoMoney;
import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney;
@ -54,10 +55,15 @@ import haveno.core.offer.OfferDirection;
import static haveno.core.offer.OfferDirection.BUY;
import haveno.core.offer.OfferFilterService;
import haveno.core.offer.OfferFilterService.Result;
import haveno.core.offer.OfferPayload;
import haveno.core.offer.OfferUtil;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager;
import haveno.core.payment.PaymentAccount;
import haveno.core.proto.persistable.CorePersistenceProtoResolver;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import static haveno.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer;
import haveno.core.user.User;
import haveno.core.util.PriceUtil;
@ -68,6 +74,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import static java.util.Comparator.comparing;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -78,6 +85,8 @@ import org.bitcoinj.core.Transaction;
@Slf4j
public class CoreOffersService {
private static final long WAIT_FOR_EDIT_REMOVAL_MS = 5000;
private final Supplier<Comparator<Offer>> priceComparator = () -> comparing(Offer::getPrice);
private final Supplier<Comparator<OpenOffer>> openOfferPriceComparator = () -> comparing(openOffer -> openOffer.getOffer().getPrice());
private final Supplier<Comparator<Offer>> reversePriceComparator = () -> comparing(Offer::getPrice).reversed();
@ -93,6 +102,8 @@ public class CoreOffersService {
private final OfferFilterService offerFilter;
private final OpenOfferManager openOfferManager;
private final User user;
private final PriceFeedService priceFeedService;
private final CorePersistenceProtoResolver corePersistenceProtoResolver;
@Inject
public CoreOffersService(CoreContext coreContext,
@ -103,7 +114,9 @@ public class CoreOffersService {
OfferFilterService offerFilter,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
User user) {
User user,
PriceFeedService priceFeedService,
CorePersistenceProtoResolver corePersistenceProtoResolver) {
this.coreContext = coreContext;
this.keyRing = keyRing;
this.coreWalletsService = coreWalletsService;
@ -112,6 +125,8 @@ public class CoreOffersService {
this.offerFilter = offerFilter;
this.openOfferManager = openOfferManager;
this.user = user;
this.priceFeedService = priceFeedService;
this.corePersistenceProtoResolver = corePersistenceProtoResolver;
}
// excludes my offers
@ -149,7 +164,7 @@ public class CoreOffersService {
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
return getMyOffers().stream()
.filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
.sorted(openOfferPriceComparator(direction, CurrencyUtil.isTraditionalCurrency(currencyCode)))
.sorted(openOfferPriceComparator(direction))
.collect(Collectors.toList());
}
@ -257,7 +272,7 @@ public class CoreOffersService {
// get payment account
if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
// get extra info
if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
@ -284,33 +299,146 @@ public class CoreOffersService {
errorMessageHandler);
}
// TODO: this implementation is missing; implement.
Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
Price price,
boolean useMarketBasedPrice,
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return createOfferService.createAndGetOffer(offerId,
direction,
currencyCode.toUpperCase(),
amount,
minAmount,
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
void editOffer(String offerId,
String currencyCode,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMarginPct,
String triggerPriceAsString,
String paymentAccountId,
String extraInfo,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
// collect offer info
final OpenOffer openOffer = getMyOffer(offerId);
final Offer offer = openOffer.getOffer();
final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload();
// get currency code
if (currencyCode.isEmpty()) currencyCode = offer.getCounterCurrencyCode();
String upperCaseCurrencyCode = currencyCode.toUpperCase();
// get payment account
if (paymentAccountId.isEmpty()) paymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); // TODO: invoke error handler for this and other offer methods
// get preselected payment account
PaymentAccount preselectedPaymentAccount = getPreselectedPaymentAccount(paymentAccount, currencyCode);
// start edit offer
OpenOffer.State initialState = openOffer.getState();
openOfferManager.editOpenOfferStart(openOffer, () -> {
try {
// wait for remove offer to propagate
// TODO: if offer edit is published too quickly, the remove message can be received after the add message, in which case the offer will be offline until the next offer refresh
HavenoUtils.waitFor(WAIT_FOR_EDIT_REMOVAL_MS);
// create edited offer
Price price = priceAsString.isEmpty() ? null : Price.valueOf(upperCaseCurrencyCode, priceStringToLong(priceAsString, upperCaseCurrencyCode));
final OfferPayload newOfferPayload = createOfferService.createAndGetOffer(offerId,
offer.getDirection(),
upperCaseCurrencyCode,
offer.getAmount(),
offer.getMinAmount(),
price,
useMarketBasedPrice,
exactMultiply(marketPriceMarginPct, 0.01),
offerPayload.getBuyerSecurityDepositPct(),
preselectedPaymentAccount,
offerPayload.isPrivateOffer(),
offer.hasBuyerAsTakerWithoutDeposit(),
extraInfo).getOfferPayload();
Offer editedOffer = getEditedOffer(openOffer, newOfferPayload);
// publish edited offer
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, upperCaseCurrencyCode);
openOfferManager.editOpenOfferPublish(editedOffer, triggerPriceAsLong, initialState, () -> {
Offer updatedEditedOffer = openOfferManager.getOpenOffer(offerId).get().getOffer(); // get latest offer
resultHandler.accept(updatedEditedOffer);
}, (errorMsg) -> {
errorMessageHandler.handleErrorMessage(errorMsg);
});
} catch (Exception e) {
errorMessageHandler.handleErrorMessage(format("Error editing offer %s: %s", offerId, e.getMessage()));
return;
}
}, errorMessageHandler);
}
private PaymentAccount getPreselectedPaymentAccount(PaymentAccount paymentAccount, String currencyCode) {
if (paymentAccount == null) throw new IllegalArgumentException("payment account cannot be null");
if (currencyCode == null || currencyCode.isEmpty()) throw new IllegalArgumentException("currency code cannot be null or empty");
Optional<TradeCurrency> optionalTradeCurrency = CurrencyUtil.getTradeCurrency(currencyCode);
if (!optionalTradeCurrency.isPresent()) throw new IllegalArgumentException(format("cannot get trade currency for currency code %s", currencyCode));
TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get();
PaymentAccount preselectedPaymentAccount = PaymentAccount.fromProto(paymentAccount.toProtoMessage(), corePersistenceProtoResolver);
if (paymentAccount.getSingleTradeCurrency() != null)
preselectedPaymentAccount.setSingleTradeCurrency(selectedTradeCurrency);
else
preselectedPaymentAccount.setSelectedTradeCurrency(selectedTradeCurrency);
return preselectedPaymentAccount;
}
public Offer getEditedOffer(OpenOffer openOffer, OfferPayload newOfferPayload) {
// editedPayload is a merge of the original offerPayload and newOfferPayload
// fields which are editable are merged in from newOfferPayload (such as payment account details)
// fields which cannot change (most importantly XMR amount) are sourced from the original offerPayload
final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload();
final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(),
offerPayload.getDate(),
offerPayload.getOwnerNodeAddress(),
offerPayload.getPubKeyRing(),
offerPayload.getDirection(),
newOfferPayload.getPrice(),
newOfferPayload.getMarketPriceMarginPct(),
newOfferPayload.isUseMarketBasedPrice(),
offerPayload.getAmount(),
offerPayload.getMinAmount(),
offerPayload.getMakerFeePct(),
offerPayload.getTakerFeePct(),
offerPayload.getPenaltyFeePct(),
offerPayload.getBuyerSecurityDepositPct(),
offerPayload.getSellerSecurityDepositPct(),
newOfferPayload.getBaseCurrencyCode(),
newOfferPayload.getCounterCurrencyCode(),
newOfferPayload.getPaymentMethodId(),
newOfferPayload.getMakerPaymentAccountId(),
newOfferPayload.getCountryCode(),
newOfferPayload.getAcceptedCountryCodes(),
newOfferPayload.getBankId(),
newOfferPayload.getAcceptedBankIds(),
offerPayload.getVersionNr(),
offerPayload.getBlockHeightAtOfferCreation(),
offerPayload.getMaxTradeLimit(),
offerPayload.getMaxTradePeriod(),
offerPayload.isUseAutoClose(),
offerPayload.isUseReOpenAfterAutoClose(),
offerPayload.getLowerClosePrice(),
offerPayload.getUpperClosePrice(),
offerPayload.isPrivateOffer(),
offerPayload.getChallengeHash(),
offerPayload.getExtraDataMap(),
offerPayload.getProtocolVersion(),
offerPayload.getArbitratorSigner(),
offerPayload.getArbitratorSignature(),
offerPayload.getReserveTxKeyImages(),
newOfferPayload.getExtraInfo());
Offer editedOffer = new Offer(editedPayload);
editedOffer.setPriceFeedService(priceFeedService);
editedOffer.setState(Offer.State.AVAILABLE);
return editedOffer;
}
void deactivateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
openOfferManager.deactivateOpenOffer(getMyOffer(offerId), false, resultHandler, errorMessageHandler);
}
void activateOffer(String offerId, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
openOfferManager.activateOpenOffer(getMyOffer(offerId), resultHandler, errorMessageHandler);
}
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -336,7 +464,7 @@ public class CoreOffersService {
String sourceOfferId,
Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode());
openOfferManager.placeOffer(offer,
useSavingsWallet,
triggerPriceAsLong,
@ -353,8 +481,7 @@ public class CoreOffersService {
if ("".equals(direction)) direction = null;
if ("".equals(currencyCode)) currencyCode = null;
var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction);
var counterAssetCode = CurrencyUtil.isCryptoCurrency(currencyCode) ? offer.getOfferPayload().getBaseCurrencyCode() : offer.getOfferPayload().getCounterCurrencyCode();
var offerInWantedCurrency = currencyCode == null || counterAssetCode.equalsIgnoreCase(currencyCode);
var offerInWantedCurrency = currencyCode == null || offer.getCounterCurrencyCode().equalsIgnoreCase(currencyCode);
return offerOfWantedDirection && offerInWantedCurrency;
}
@ -366,17 +493,12 @@ public class CoreOffersService {
: 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 seller probably wants to see buy orders in price descending order.
if (isTraditional)
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
else
return direction.equalsIgnoreCase(OfferDirection.SELL.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
}
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.PaymentAccountFactory;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.payment.validation.InteracETransferValidator;
import haveno.core.trade.HavenoUtils;
import haveno.core.user.User;
import java.io.File;
import static java.lang.String.format;
@ -48,19 +50,24 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
@Slf4j
class CorePaymentAccountsService {
public class CorePaymentAccountsService {
private final CoreAccountService accountService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final User user;
public final InteracETransferValidator interacETransferValidator;
@Inject
public CorePaymentAccountsService(CoreAccountService accountService,
AccountAgeWitnessService accountAgeWitnessService,
User user) {
User user,
InteracETransferValidator interacETransferValidator) {
this.accountService = accountService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.user = user;
this.interacETransferValidator = interacETransferValidator;
HavenoUtils.corePaymentAccountService = this;
}
PaymentAccount createPaymentAccount(PaymentAccountForm form) {

View file

@ -74,9 +74,11 @@ class CorePriceService {
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
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 {
return priceFeedService.requestAllPrices().values().stream()
.map(marketPrice -> {
double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice);
return new MarketPriceInfo(marketPrice.getCurrencyCode(), marketPrice.getPrice());
})
.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.
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
// 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
// ordering, we have to reverse the price comparator.
boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode);
if (isCrypto) offerPriceComparator = offerPriceComparator.reversed();
//boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode);
//if (isCrypto) offerPriceComparator = offerPriceComparator.reversed();
// Offer amounts are used for the secondary sort. They are sorted from high to low.
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);
accumulatedAmount += amount;
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;
LinkedHashMap<Double,Double> sellTM = new LinkedHashMap<Double,Double>();
for(Offer offer: sellOffers){
@ -141,7 +143,7 @@ class CorePriceService {
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
accumulatedAmount += amount;
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);
}
/**
* 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);
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
String currencyCode = offer.getCurrencyCode();
String currencyCode = offer.getCounterCurrencyCode();
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 (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
} else if (offer.isTraditionalOffer()
&& !amount.equals(offer.getMinAmount()) && !amount.equals(amount)) {
// 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());
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount);
} else if (offer.isTraditionalOffer() && offer.isRange()) {
amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId());
}
}
@ -192,7 +189,6 @@ class CoreTradesService {
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
log.info("Keeping funds received from trade {}", tradeId);
tradeManager.onTradeCompleted(trade);
}

View file

@ -173,12 +173,24 @@ class CoreWalletsService {
}
}
String relayXmrTx(String metadata) {
List<MoneroTxWallet> createXmrSweepTxs(String address) {
accountService.checkAccountOpen();
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
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) {
log.error("", ex);
throw new IllegalStateException(ex);

View file

@ -24,6 +24,7 @@ import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.common.config.BaseCurrencyNetwork;
import haveno.common.config.Config;
import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
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 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 int MAX_CONSECUTIVE_ERRORS = 3; // max errors before switching connections
private static int numConsecutiveErrors = 0;
public enum XmrConnectionError {
public enum XmrConnectionFallbackType {
LOCAL,
CUSTOM
CUSTOM,
PROVIDED
}
private final Object lock = new Object();
@ -92,12 +96,12 @@ public final class XmrConnectionService {
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
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 LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener();
@Getter
private final ObjectProperty<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>();
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0);
@ -105,15 +109,15 @@ public final class XmrConnectionService {
private boolean isInitialized;
private boolean pollInProgress;
private MoneroDaemonRpc daemon;
private MoneroDaemonRpc monerod;
private Boolean isConnected = false;
@Getter
private MoneroDaemonInfo lastInfo;
private Long lastFallbackInvocation;
private Long lastLogPollErrorTimestamp;
private long lastLogDaemonNotSyncedTimestamp;
private long lastLogMonerodNotSyncedTimestamp;
private Long syncStartHeight;
private TaskLooper daemonPollLooper;
private TaskLooper monerodPollLooper;
private long lastRefreshPeriodMs;
@Getter
private boolean isShutDownStarted;
@ -129,6 +133,7 @@ public final class XmrConnectionService {
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
private boolean fallbackApplied;
private boolean usedSyncingLocalNodeBeforeStartup;
@Inject
public XmrConnectionService(P2PService p2PService,
@ -156,7 +161,13 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override
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
public void onHiddenServicePublished() {}
@ -180,16 +191,16 @@ public final class XmrConnectionService {
log.info("Shutting down {}", getClass().getSimpleName());
isInitialized = false;
synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop();
daemon = null;
if (monerodPollLooper != null) monerodPollLooper.stop();
monerod = null;
}
}
// ------------------------ CONNECTION MANAGEMENT -------------------------
public MoneroDaemonRpc getDaemon() {
public MoneroDaemonRpc getMonerod() {
accountService.checkAccountOpen();
return this.daemon;
return this.monerod;
}
public String getProxyUri() {
@ -270,7 +281,7 @@ public final class XmrConnectionService {
accountService.checkAccountOpen();
// 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");
return null;
}
@ -283,6 +294,10 @@ public final class XmrConnectionService {
return bestConnection;
}
private boolean fallbackRequiredBeforeConnectionSwitch() {
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
}
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
}
@ -390,11 +405,21 @@ public final class XmrConnectionService {
}
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 (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced");
}
public Long getHeight() {
if (lastInfo == null) return null;
return lastInfo.getHeight();
}
public Long getTargetHeight() {
if (lastInfo == null) return null;
return lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight();
}
public boolean isSyncedWithinTolerance() {
Long targetHeight = getTargetHeight();
if (targetHeight == null) return false;
@ -402,11 +427,6 @@ public final class XmrConnectionService {
return false;
}
public Long getTargetHeight() {
if (lastInfo == null) return null;
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
}
public XmrKeyImagePoller getKeyImagePoller() {
synchronized (lock) {
if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
@ -433,6 +453,7 @@ public final class XmrConnectionService {
}
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();
}
@ -458,15 +479,20 @@ public final class XmrConnectionService {
public void fallbackToBestConnection() {
if (isShutDownStarted) return;
if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
fallbackApplied = true;
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
log.warn("Falling back to public nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
initializeConnections();
} else {
log.warn("Falling back to provided nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
initializeConnections();
if (getConnection() == null) {
log.warn("No provided nodes available, falling back to public nodes");
fallbackToBestConnection();
}
}
fallbackApplied = true;
initializeConnections();
}
// ------------------------------- HELPERS --------------------------------
@ -548,8 +574,8 @@ public final class XmrConnectionService {
// register local node listener
xmrLocalNode.addListener(new XmrLocalNodeListener() {
@Override
public void onNodeStarted(MoneroDaemonRpc daemon) {
log.info("Local monero node started, height={}", daemon.getHeight());
public void onNodeStarted(MoneroDaemonRpc monerod) {
log.info("Local monero node started, height={}", monerod.getHeight());
}
@Override
@ -578,8 +604,8 @@ public final class XmrConnectionService {
setConnection(connection.getUri());
// reset error connecting to local node
if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) {
connectionServiceError.set(null);
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
connectionServiceFallbackType.set(null);
}
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestConnection();
@ -602,8 +628,10 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
}
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -615,8 +643,10 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection);
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection);
}
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -632,6 +662,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
log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@ -666,43 +701,30 @@ public final class XmrConnectionService {
onConnectionChanged(connectionManager.getConnection());
}
private boolean lastUsedLocalSyncingNode() {
return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored();
}
public void startLocalNode() {
public void startLocalNode() throws Exception {
// cannot start local node as seed node
if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node");
}
// start local node if offline and used as last connection
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
try {
log.info("Starting local node");
xmrLocalNode.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");
}
// start local node
log.info("Starting local node");
xmrLocalNode.start();
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return;
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) {
if (currentConnection == null) {
daemon = null;
monerod = null;
isConnected = false;
connectionList.setCurrentConnectionUri(null);
} else {
daemon = new MoneroDaemonRpc(currentConnection);
monerod = new MoneroDaemonRpc(currentConnection);
isConnected = currentConnection.isConnected();
connectionList.removeConnection(currentConnection.getUri());
connectionList.addConnection(currentConnection);
@ -717,11 +739,11 @@ public final class XmrConnectionService {
}
// update key image poller
keyImagePoller.setDaemon(getDaemon());
keyImagePoller.setMonerod(getMonerod());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling
doPollDaemon();
tryPollMonerod();
if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
@ -741,74 +763,100 @@ public final class XmrConnectionService {
private void startPolling() {
synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemon());
daemonPollLooper.start(getInternalRefreshPeriodMs());
if (monerodPollLooper != null) monerodPollLooper.stop();
monerodPollLooper = new TaskLooper(() -> {
if (!pollInProgress) {
tryPollMonerod();
}
});
monerodPollLooper.start(getInternalRefreshPeriodMs());
}
}
private void stopPolling() {
synchronized (lock) {
if (daemonPollLooper != null) {
daemonPollLooper.stop();
daemonPollLooper = null;
if (monerodPollLooper != null) {
monerodPollLooper.stop();
monerodPollLooper = null;
}
}
}
private void pollDaemon() {
if (pollInProgress) return;
doPollDaemon();
private void tryPollMonerod() {
try {
pollMonerod();
} catch (Exception e) {
// error is already handled
}
}
private void doPollDaemon() {
/**
* Polls monerod for the latest info and updates the connection if necessary.
*/
private void pollMonerod() {
synchronized (pollLock) {
pollInProgress = true;
if (isShutDownStarted) return;
try {
// poll daemon
if (daemon == null) switchToBestConnection();
// poll monerod
if (monerod == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
try {
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo();
if (monerod == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = monerod.getInfo();
numConsecutiveErrors = 0;
} catch (Exception e) {
// skip handling if shutting down
if (isShutDownStarted) return;
// skip error handling up to max attempts
numConsecutiveErrors++;
if (numConsecutiveErrors <= MAX_CONSECUTIVE_ERRORS) {
return;
} else {
numConsecutiveErrors = 0; // reset error count
}
// invoke fallback handling on startup error
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode();
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (lastInfo == null && canFallback) {
if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
lastFallbackInvocation = System.currentTimeMillis();
if (lastUsedLocalSyncingNode()) {
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
connectionServiceError.set(XmrConnectionError.LOCAL);
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 daemon info from custom connection on startup: " + e.getMessage());
connectionServiceError.set(XmrConnectionError.CUSTOM);
log.warn("Failed to fetch monerod info from custom connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
}
}
return;
}
// log error message periodically
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage());
if (lastWarningOutsidePeriod()) {
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));
lastLogPollErrorTimestamp = System.currentTimeMillis();
}
// switch to best connection
switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = daemon.getInfo(); // caught internally if still fails
if (monerod == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = monerod.getInfo(); // caught internally if still fails
}
// connected to daemon
// connected to monerod
isConnected = true;
connectionServiceError.set(null);
connectionServiceFallbackType.set(null);
// set chain height
chainHeight.set(lastInfo.getHeight());
// 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
@ -816,10 +864,10 @@ public final class XmrConnectionService {
// write sync status to preferences
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
// throttle warnings if daemon not synced
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) {
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight());
lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis();
// throttle warnings if monerod not synced
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: {}", getHeight(), getTargetHeight());
lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis();
}
// announce connection change if refresh period changes
@ -829,11 +877,11 @@ public final class XmrConnectionService {
return;
}
// update properties on user thread
UserThread.execute(() -> {
// get the number of connections, which is only available if not restricted
int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections();
// set chain height
chainHeight.set(lastInfo.getHeight());
// updates on user thread
UserThread.execute(() -> {
// update sync progress
boolean isTestnet = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL;
@ -854,15 +902,22 @@ public final class XmrConnectionService {
}
}
connections.set(availableConnections);
numConnections.set(availableConnections.size());
numConnections.set(numOutgoingConnections);
// notify update
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
if (lastLogPollErrorTimestamp != null) {
log.info("Successfully fetched daemon info after previous error");
log.info("Successfully fetched monerod info after previous error");
lastLogPollErrorTimestamp = null;
}
@ -870,25 +925,41 @@ public final class XmrConnectionService {
getConnectionServiceErrorMsg().set(null);
} catch (Exception e) {
// not connected to daemon
// not connected to monerod
isConnected = false;
// skip if shut down
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
getConnectionServiceErrorMsg().set(e.getMessage());
getConnectionServiceErrorMsg().set(errorMsg);
throw e;
} finally {
pollInProgress = false;
}
}
}
private boolean lastWarningOutsidePeriod() {
return lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS;
}
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() {
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() {
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;
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;
break;
}
}
if (!hasConfiguredLocalNode) return true;
return false;
return !hasConfiguredLocalNode;
}
public void addListener(XmrLocalNodeListener listener) {

View file

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

View file

@ -78,7 +78,15 @@ public final class PaymentAccountForm implements PersistablePayload {
CASH_APP,
PAYPAL,
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) {
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) ->
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).
private final OfferInfo offer;
@ -91,14 +91,19 @@ public class TradeInfo implements Payload {
private final boolean isDepositsPublished;
private final boolean isDepositsConfirmed;
private final boolean isDepositsUnlocked;
private final boolean isDepositsFinalized;
private final boolean isPaymentSent;
private final boolean isPaymentReceived;
private final boolean isPayoutPublished;
private final boolean isPayoutConfirmed;
private final boolean isPayoutUnlocked;
private final boolean isPayoutFinalized;
private final boolean isCompleted;
private final String contractAsJson;
private final ContractInfo contract;
private final long startTime;
private final long maxDurationMs;
private final long deadlineTime;
public TradeInfo(TradeInfoV1Builder builder) {
this.offer = builder.getOffer();
@ -132,14 +137,19 @@ public class TradeInfo implements Payload {
this.isDepositsPublished = builder.isDepositsPublished();
this.isDepositsConfirmed = builder.isDepositsConfirmed();
this.isDepositsUnlocked = builder.isDepositsUnlocked();
this.isDepositsFinalized = builder.isDepositsFinalized();
this.isPaymentSent = builder.isPaymentSent();
this.isPaymentReceived = builder.isPaymentReceived();
this.isPayoutPublished = builder.isPayoutPublished();
this.isPayoutConfirmed = builder.isPayoutConfirmed();
this.isPayoutUnlocked = builder.isPayoutUnlocked();
this.isPayoutFinalized = builder.isPayoutFinalized();
this.isCompleted = builder.isCompleted();
this.contractAsJson = builder.getContractAsJson();
this.contract = builder.getContract();
this.startTime = builder.getStartTime();
this.maxDurationMs = builder.getMaxDurationMs();
this.deadlineTime = builder.getDeadlineTime();
}
public static TradeInfo toTradeInfo(Trade trade) {
@ -193,15 +203,20 @@ public class TradeInfo implements Payload {
.withIsDepositsPublished(trade.isDepositsPublished())
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
.withIsDepositsFinalized(trade.isDepositsFinalized())
.withIsPaymentSent(trade.isPaymentSent())
.withIsPaymentReceived(trade.isPaymentReceived())
.withIsPayoutPublished(trade.isPayoutPublished())
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
.withIsPayoutFinalized(trade.isPayoutFinalized())
.withIsCompleted(trade.isCompleted())
.withContractAsJson(trade.getContractAsJson())
.withContract(contractInfo)
.withOffer(toOfferInfo(trade.getOffer()))
.withStartTime(trade.getStartDate().getTime())
.withMaxDurationMs(trade.getMaxTradePeriod())
.withDeadlineTime(trade.getMaxTradePeriodDate().getTime())
.build();
}
@ -243,14 +258,19 @@ public class TradeInfo implements Payload {
.setIsDepositsPublished(isDepositsPublished)
.setIsDepositsConfirmed(isDepositsConfirmed)
.setIsDepositsUnlocked(isDepositsUnlocked)
.setIsDepositsFinalized(isDepositsFinalized)
.setIsPaymentSent(isPaymentSent)
.setIsPaymentReceived(isPaymentReceived)
.setIsCompleted(isCompleted)
.setIsPayoutPublished(isPayoutPublished)
.setIsPayoutConfirmed(isPayoutConfirmed)
.setIsPayoutUnlocked(isPayoutUnlocked)
.setIsPayoutFinalized(isPayoutFinalized)
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
.setContract(contract.toProtoMessage())
.setStartTime(startTime)
.setMaxDurationMs(maxDurationMs)
.setDeadlineTime(deadlineTime)
.build();
}
@ -287,14 +307,19 @@ public class TradeInfo implements Payload {
.withIsDepositsPublished(proto.getIsDepositsPublished())
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
.withIsDepositsFinalized(proto.getIsDepositsFinalized())
.withIsPaymentSent(proto.getIsPaymentSent())
.withIsPaymentReceived(proto.getIsPaymentReceived())
.withIsCompleted(proto.getIsCompleted())
.withIsPayoutPublished(proto.getIsPayoutPublished())
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
.withIsPayoutFinalized(proto.getIsPayoutFinalized())
.withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract())))
.withStartTime(proto.getStartTime())
.withMaxDurationMs(proto.getMaxDurationMs())
.withDeadlineTime(proto.getDeadlineTime())
.build();
}
@ -330,15 +355,20 @@ public class TradeInfo implements Payload {
", isDepositsPublished=" + isDepositsPublished + "\n" +
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
", isDepositsFinalized=" + isDepositsFinalized + "\n" +
", isPaymentSent=" + isPaymentSent + "\n" +
", isPaymentReceived=" + isPaymentReceived + "\n" +
", isPayoutPublished=" + isPayoutPublished + "\n" +
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
", isPayoutFinalized=" + isPayoutFinalized + "\n" +
", isCompleted=" + isCompleted + "\n" +
", offer=" + offer + "\n" +
", contractAsJson=" + contractAsJson + "\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 isDepositsConfirmed;
private boolean isDepositsUnlocked;
private boolean isDepositsFinalized;
private boolean isPaymentSent;
private boolean isPaymentReceived;
private boolean isPayoutPublished;
private boolean isPayoutConfirmed;
private boolean isPayoutUnlocked;
private boolean isPayoutFinalized;
private boolean isCompleted;
private String contractAsJson;
private ContractInfo contract;
private String closingStatus;
private long startTime;
private long maxDurationMs;
private long deadlineTime;
public TradeInfoV1Builder withOffer(OfferInfo offer) {
this.offer = offer;
@ -239,6 +244,11 @@ public final class TradeInfoV1Builder {
return this;
}
public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) {
this.isDepositsFinalized = isDepositsFinalized;
return this;
}
public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) {
this.isPaymentSent = isPaymentSent;
return this;
@ -264,6 +274,11 @@ public final class TradeInfoV1Builder {
return this;
}
public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) {
this.isPayoutFinalized = isPayoutFinalized;
return this;
}
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
this.isCompleted = isCompleted;
return this;
@ -284,6 +299,21 @@ public final class TradeInfoV1Builder {
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() {
return new TradeInfo(this);
}

View file

@ -68,6 +68,8 @@ public class AvoidStandbyModeService {
this.inhibitorPathSpec = inhibitorPath();
preferences.getUseStandbyModeProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
isStopped = true;
log.info("AvoidStandbyModeService stopped");
if (Utilities.isLinux() && runningInhibitorProcess().isPresent()) {
Objects.requireNonNull(stopLinuxInhibitorCountdownLatch).countDown();
}

View file

@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
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.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: 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.api.CoreContext;
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.locale.Res;
import haveno.core.offer.OpenOfferManager;
@ -115,7 +115,7 @@ import org.fxmisc.easybind.monadic.MonadicBinding;
public class HavenoSetup {
private static final String VERSION_FILE_NAME = "version";
private static final long STARTUP_TIMEOUT_MINUTES = 4;
private static final int STARTUP_TIMEOUT_MINUTES = 5;
private final DomainInitialisation domainInitialisation;
private final P2PNetworkSetup p2PNetworkSetup;
@ -159,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler;
@Setter
@Nullable
private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler;
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter
@Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -431,9 +431,9 @@ public class HavenoSetup {
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
// listen for fallback handling
getConnectionServiceError().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionErrorHandler == null) return;
displayMoneroConnectionErrorHandler.accept(newValue);
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionFallbackHandler == null) return;
displayMoneroConnectionFallbackHandler.accept(newValue);
});
log.info("Init P2P network");
@ -735,8 +735,8 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg();
}
public ObjectProperty<XmrConnectionError> getConnectionServiceError() {
return xmrConnectionService.getConnectionServiceError();
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceFallbackType();
}
public StringProperty getTopErrorMsg() {

View file

@ -94,7 +94,7 @@ public class P2PNetworkSetup {
if (warning != null && p2pPeers == 0) {
result = warning;
} 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) {
result = p2pInfo;
} else if (p2pPeers == 0)

View file

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

View file

@ -151,23 +151,23 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
});
});
// shut down trade and wallet services
log.info("Shutting down trade and wallet services");
injector.getInstance(OfferBookService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown();
});
// shut down trade and wallet services
log.info("Shutting down trade and wallet services");
injector.getInstance(OfferBookService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown();
});
// we wait max 5 sec.
UserThread.runAfter(() -> {
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
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);
});
}, 5);

View file

@ -84,9 +84,9 @@ public class FilterManager {
private final ConfigFileEditor configFileEditor;
private final ProvidersRepository providersRepository;
private final boolean ignoreDevMsg;
private final boolean useDevPrivilegeKeys;
private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private final List<String> publicKeys;
private ECKey filterSigningKey;
private final Set<Filter> invalidFilters = new HashSet<>();
private Consumer<String> filterWarningHandler;
@ -113,16 +113,31 @@ public class FilterManager {
this.configFileEditor = new ConfigFileEditor(config.configFile);
this.providersRepository = providersRepository;
this.ignoreDevMsg = ignoreDevMsg;
publicKeys = useDevPrivilegeKeys ?
Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) :
List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27",
"029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f",
"034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9");
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
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
@ -587,16 +602,16 @@ public class FilterManager {
"but the new version does not recognize it as valid filter): " +
"signerPubKeyAsHex from filter is not part of our pub key list. " +
"signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}",
signerPubKeyAsHex, publicKeys, new Date(filter.getCreationDate()));
signerPubKeyAsHex, getPubKeyList(), new Date(filter.getCreationDate()));
return false;
}
return true;
}
private boolean isPublicKeyInList(String pubKeyAsHex) {
boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex);
boolean isPublicKeyInList = getPubKeyList().contains(pubKeyAsHex);
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;
}

View file

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

View file

@ -66,7 +66,7 @@ import static java.lang.String.format;
@Slf4j
public class CurrencyUtil {
public static void setup() {
setBaseCurrencyCode("XMR");
setBaseCurrencyCode(baseCurrencyCode);
}
private static final AssetRegistry assetRegistry = new AssetRegistry();
@ -93,7 +93,7 @@ public class CurrencyUtil {
public static Collection<TraditionalCurrency> getAllSortedFiatCurrencies(Comparator comparator) {
return getAllSortedTraditionalCurrencies(comparator).stream()
.filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode()))
.collect(Collectors.toList()); // sorted by currency name
.collect(Collectors.toList()); // sorted by currency name
}
public static List<TradeCurrency> getAllFiatCurrencies() {
@ -105,11 +105,11 @@ public class CurrencyUtil {
public static List<TradeCurrency> getAllSortedFiatCurrencies() {
return getAllSortedTraditionalCurrencies().stream()
.filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode()))
.collect(Collectors.toList()); // sorted by currency name
.collect(Collectors.toList()); // sorted by currency name
}
public static Collection<TraditionalCurrency> getAllSortedTraditionalCurrencies() {
return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
}
public static List<TradeCurrency> getAllTraditionalCurrencies() {
@ -198,12 +198,16 @@ public class CurrencyUtil {
final List<CryptoCurrency> result = new ArrayList<>();
result.add(new CryptoCurrency("BTC", "Bitcoin"));
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("DOGE", "Dogecoin"));
result.add(new CryptoCurrency("ETH", "Ether"));
result.add(new CryptoCurrency("LTC", "Litecoin"));
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)"));
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)"));
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)"));
result.add(new CryptoCurrency("XRP", "Ripple"));
result.add(new CryptoCurrency("ADA", "Cardano"));
result.add(new CryptoCurrency("SOL", "Solana"));
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);
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.
* 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.
@ -406,6 +410,13 @@ public class CurrencyUtil {
removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na");
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 {
return Currency.getInstance(currencyCode).getDisplayName();
} catch (Throwable t) {
@ -507,17 +518,11 @@ public class CurrencyUtil {
}
public static String getCurrencyPair(String currencyCode) {
if (isTraditionalCurrency(currencyCode))
return Res.getBaseCurrencyCode() + "/" + currencyCode;
else
return currencyCode + "/" + Res.getBaseCurrencyCode();
return Res.getBaseCurrencyCode() + "/" + currencyCode;
}
public static String getCounterCurrency(String currencyCode) {
if (isTraditionalCurrency(currencyCode))
return currencyCode;
else
return Res.getBaseCurrencyCode();
return currencyCode;
}
public static String getPriceWithCurrencyCode(String currencyCode) {
@ -525,10 +530,7 @@ public class CurrencyUtil {
}
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());
}
public static String getOfferVolumeCode(String currencyCode) {

View file

@ -103,7 +103,11 @@ public class Res {
}
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) {

View file

@ -86,7 +86,7 @@ public final class TraditionalCurrency extends TradeCurrency {
}
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 CryptoMoney crypto;
public final CryptoMoney cryptoMoney;
/**
* 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.currencyCode != null, "currency code required");
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.
*/
public CryptoMoney coinToCrypto(Coin convertCoin) {
BigInteger converted = BigInteger.valueOf(coin.value)
.multiply(BigInteger.valueOf(convertCoin.value))
.divide(BigInteger.valueOf(crypto.value));
final BigInteger converted = BigInteger.valueOf(convertCoin.value)
.multiply(BigInteger.valueOf(cryptoMoney.value))
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
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.
*/
public Coin cryptoToCoin(CryptoMoney convertCrypto) {
checkArgument(convertCrypto.currencyCode.equals(crypto.currencyCode), "Currency mismatch: %s vs %s",
convertCrypto.currencyCode, crypto.currencyCode);
checkArgument(convertCrypto.currencyCode.equals(cryptoMoney.currencyCode), "Currency mismatch: %s vs %s",
convertCrypto.currencyCode, cryptoMoney.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing.
BigInteger converted = BigInteger.valueOf(crypto.value)
.multiply(BigInteger.valueOf(convertCrypto.value))
.divide(BigInteger.valueOf(coin.value));
final BigInteger converted = BigInteger.valueOf(convertCrypto.value).multiply(BigInteger.valueOf(coin.value))
.divide(BigInteger.valueOf(cryptoMoney.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");

View file

@ -136,7 +136,7 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
public String toFriendlyString() {
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;
}

View file

@ -15,84 +15,84 @@
* 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 java.io.Serializable;
import java.math.BigInteger;
import org.bitcoinj.core.Coin;
import com.google.common.base.Objects;
/**
* An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount.
*/
public class TraditionalExchangeRate implements Serializable {
public final Coin coin;
public final TraditionalMoney traditionalMoney;
/** Construct exchange rate. This amount of coin is worth that amount of money. */
public TraditionalExchangeRate(Coin coin, TraditionalMoney traditionalMoney) {
checkArgument(coin.isPositive());
checkArgument(traditionalMoney.isPositive());
checkArgument(traditionalMoney.currencyCode != null, "currency code required");
this.coin = coin;
this.traditionalMoney = traditionalMoney;
}
/** Construct exchange rate. One coin is worth this amount of traditional money. */
public TraditionalExchangeRate(TraditionalMoney traditionalMoney) {
this(Coin.COIN, traditionalMoney);
}
/**
* Convert a coin amount to a traditional money amount using this exchange rate.
* @throws ArithmeticException if the converted amount is too high or too low.
*/
public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) {
// 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))
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return TraditionalMoney.valueOf(traditionalMoney.currencyCode, converted.longValue());
}
/**
* Convert a traditional money amount to a coin amount using this exchange rate.
* @throws ArithmeticException if the converted coin amount is too high or too low.
*/
public Coin traditionalMoneyToCoin(TraditionalMoney convertTraditionalMoney) {
checkArgument(convertTraditionalMoney.currencyCode.equals(traditionalMoney.currencyCode), "Currency mismatch: %s vs %s",
convertTraditionalMoney.currencyCode, traditionalMoney.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing.
final BigInteger converted = BigInteger.valueOf(convertTraditionalMoney.value).multiply(BigInteger.valueOf(coin.value))
.divide(BigInteger.valueOf(traditionalMoney.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
try {
return Coin.valueOf(converted.longValue());
} catch (IllegalArgumentException x) {
throw new ArithmeticException("Overflow: " + x.getMessage());
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TraditionalExchangeRate other = (TraditionalExchangeRate) o;
return Objects.equal(this.coin, other.coin) && Objects.equal(this.traditionalMoney, other.traditionalMoney);
}
@Override
public int hashCode() {
return Objects.hashCode(coin, traditionalMoney);
}
}
import static com.google.common.base.Preconditions.checkArgument;
import java.io.Serializable;
import java.math.BigInteger;
import org.bitcoinj.core.Coin;
import com.google.common.base.Objects;
/**
* An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount.
*/
public class TraditionalExchangeRate implements Serializable {
public final Coin coin;
public final TraditionalMoney traditionalMoney;
/** Construct exchange rate. This amount of coin is worth that amount of money. */
public TraditionalExchangeRate(Coin coin, TraditionalMoney traditionalMoney) {
checkArgument(coin.isPositive());
checkArgument(traditionalMoney.isPositive());
checkArgument(traditionalMoney.currencyCode != null, "currency code required");
this.coin = coin;
this.traditionalMoney = traditionalMoney;
}
/** Construct exchange rate. One coin is worth this amount of traditional money. */
public TraditionalExchangeRate(TraditionalMoney traditionalMoney) {
this(Coin.COIN, traditionalMoney);
}
/**
* Convert a coin amount to a traditional money amount using this exchange rate.
* @throws ArithmeticException if the converted amount is too high or too low.
*/
public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) {
// 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))
.divide(BigInteger.valueOf(coin.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
return TraditionalMoney.valueOf(traditionalMoney.currencyCode, converted.longValue());
}
/**
* Convert a traditional money amount to a coin amount using this exchange rate.
* @throws ArithmeticException if the converted coin amount is too high or too low.
*/
public Coin traditionalMoneyToCoin(TraditionalMoney convertTraditionalMoney) {
checkArgument(convertTraditionalMoney.currencyCode.equals(traditionalMoney.currencyCode), "Currency mismatch: %s vs %s",
convertTraditionalMoney.currencyCode, traditionalMoney.currencyCode);
// Use BigInteger because it's much easier to maintain full precision without overflowing.
final BigInteger converted = BigInteger.valueOf(convertTraditionalMoney.value).multiply(BigInteger.valueOf(coin.value))
.divide(BigInteger.valueOf(traditionalMoney.value));
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new ArithmeticException("Overflow");
try {
return Coin.valueOf(converted.longValue());
} catch (IllegalArgumentException x) {
throw new ArithmeticException("Overflow: " + x.getMessage());
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TraditionalExchangeRate other = (TraditionalExchangeRate) o;
return Objects.equal(this.coin, other.coin) && Objects.equal(this.traditionalMoney, other.traditionalMoney);
}
@Override
public int hashCode() {
return Objects.hashCode(coin, traditionalMoney);
}
}

View file

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

View file

@ -59,7 +59,7 @@ public class TradeEvents {
}
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()) {
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
String msg = null;
@ -70,6 +70,7 @@ public class TradeEvents {
case DEPOSITS_PUBLISHED:
break;
case DEPOSITS_UNLOCKED:
case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized?
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
break;

View file

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

View file

@ -22,7 +22,6 @@ import com.google.inject.Singleton;
import haveno.common.app.Version;
import haveno.common.crypto.PubKeyRingProvider;
import haveno.common.util.Utilities;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.monetary.Price;
import haveno.core.payment.PaymentAccount;
@ -35,6 +34,7 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
@ -92,7 +92,6 @@ public class CreateOfferService {
Version.VERSION.replace(".", "");
}
// TODO: add trigger price?
public Offer createAndGetOffer(String offerId,
OfferDirection direction,
String currencyCode,
@ -134,10 +133,12 @@ public class CreateOfferService {
// must nullify empty string so contracts match
if ("".equals(extraInfo)) extraInfo = null;
// verify buyer as taker security deposit
// verify config for private no deposit offers
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
if (buyerAsTakerWithoutDeposit || isPrivateOffer) {
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
@ -149,15 +150,16 @@ public class CreateOfferService {
// verify price
boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) &&
isExternalPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
if (fixedPrice == null && !useMarketBasedPriceValue) {
throw new IllegalArgumentException("Must provide fixed price");
}
// adjust amount and min amount
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit);
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
String challenge = null;
@ -173,16 +175,15 @@ public class CreateOfferService {
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L;
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode();
String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
String baseCurrencyCode = Res.getBaseCurrencyCode();
String counterCurrencyCode = currencyCode;
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
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 useReOpenAfterAutoClose = false;
long lowerClosePrice = 0;
@ -204,8 +205,8 @@ public class CreateOfferService {
useMarketBasedPriceValue,
amountAsLong,
minAmountAsLong,
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.getMakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
HavenoUtils.getTakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
HavenoUtils.PENALTY_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
securityDepositPct,
@ -219,7 +220,7 @@ public class CreateOfferService {
acceptedBanks,
Version.VERSION,
xmrWalletService.getHeight(),
maxTradeLimit,
maxTradeLimitAsLong,
maxTradePeriod,
useAutoClose,
useReOpenAfterAutoClose,
@ -239,7 +240,6 @@ public class CreateOfferService {
return offer;
}
// TODO: add trigger price?
public Offer createClonedOffer(Offer sourceOffer,
String currencyCode,
Price fixedPrice,
@ -336,7 +336,7 @@ public class CreateOfferService {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isMarketPriceAvailable(String currencyCode) {
private boolean isExternalPriceAvailable(String currencyCode) {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
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.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.PriceUtil;
import haveno.core.util.VolumeUtil;
import haveno.core.util.coin.CoinUtil;
import haveno.network.p2p.NodeAddress;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
@ -173,32 +175,27 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Nullable
public Price getPrice() {
String currencyCode = getCurrencyCode();
String counterCurrencyCode = getCounterCurrencyCode();
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");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
MarketPrice marketPrice = priceFeedService.getMarketPrice(counterCurrencyCode);
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
double factor;
double marketPriceMargin = offerPayload.getMarketPriceMarginPct();
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
factor = getDirection() == OfferDirection.SELL ?
1 - marketPriceMargin : 1 + marketPriceMargin;
} else {
factor = getDirection() == OfferDirection.BUY ?
1 - marketPriceMargin : 1 + marketPriceMargin;
}
factor = getDirection() == OfferDirection.BUY ?
1 - marketPriceMargin : 1 + marketPriceMargin;
double marketPriceAsDouble = marketPrice.getPrice();
double targetPriceAsDouble = marketPriceAsDouble * factor;
try {
int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
int precision = CurrencyUtil.isTraditionalCurrency(counterCurrencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT;
double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision);
final long roundedToLong = MathUtils.roundDoubleToLong(scaled);
return Price.valueOf(currencyCode, roundedToLong);
return Price.valueOf(counterCurrencyCode, roundedToLong);
} catch (Exception e) {
log.error("Exception at getPrice / parseToFiat: " + e + "\n" +
"That case should never happen.");
@ -224,7 +221,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
return;
}
Price tradePrice = Price.valueOf(getCurrencyCode(), price);
Price tradePrice = Price.valueOf(getCounterCurrencyCode(), price);
Price offerPrice = getPrice();
if (offerPrice == null)
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);
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
getShortId(), getCurrencyCode(), price, offerPrice.getValue(),
getShortId(), getCounterCurrencyCode(), price, offerPrice.getValue(),
deviation * 100 + "%");
if (deviation > PRICE_TOLERANCE) {
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
@ -251,12 +248,13 @@ public class Offer implements NetworkPayload, PersistablePayload {
}
@Nullable
public Volume getVolumeByAmount(BigInteger amount) {
public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) {
Price price = getPrice();
if (price == null || amount == 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());
return volumeByAmount;
@ -385,12 +383,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Nullable
public Volume getVolume() {
return getVolumeByAmount(getAmount());
return getVolumeByAmount(getAmount(), getMinAmount(), getAmount());
}
@Nullable
public Volume getMinVolume() {
return getVolumeByAmount(getMinAmount());
return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount());
}
public boolean isBuyOffer() {
@ -507,23 +505,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getCountryCode();
}
public String getCurrencyCode() {
if (currencyCode != null) {
return currencyCode;
}
currencyCode = offerPayload.getBaseCurrencyCode().equals("XMR") ?
offerPayload.getCounterCurrencyCode() :
offerPayload.getBaseCurrencyCode();
return currencyCode;
public String getBaseCurrencyCode() {
return isInverted() ? offerPayload.getCounterCurrencyCode() : offerPayload.getBaseCurrencyCode(); // legacy offers inverted crypto
}
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() {
return offerPayload.getBaseCurrencyCode();
public boolean isInverted() {
return !offerPayload.getBaseCurrencyCode().equals("XMR");
}
public String getPaymentMethodId() {
@ -584,21 +577,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
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() {
return CurrencyUtil.isTraditionalCurrency(currencyCode);
}

View file

@ -149,6 +149,20 @@ public class OfferBookService {
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
announceOfferRemoved(offer);
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
}
});
}, OfferBookService.class.getSimpleName());
@ -257,7 +271,7 @@ public class OfferBookService {
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
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());
}
@ -298,20 +312,6 @@ public class OfferBookService {
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
}
private boolean hasValidOffer(String offerId) {
@ -404,7 +404,7 @@ public class OfferBookService {
}
// 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
// That should only be possible if the price feed provider is not available
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 -> {
try {
return new OfferForJson(offer.getDirection(),
offer.getCurrencyCode(),
offer.getCounterCurrencyCode(),
offer.getMinAmount(),
offer.getAmount(),
offer.getPrice(),

View file

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

View file

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

View file

@ -491,10 +491,10 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer +
",\r\n challengeHash='" + challengeHash +
",\r\n challengeHash='" + challengeHash + '\'' +
",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\r\n extraInfo='" + extraInfo +
",\r\n extraInfo='" + extraInfo + '\'' +
"\r\n} ";
}

View file

@ -56,12 +56,13 @@ import haveno.core.payment.PayPalAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.provider.price.MarketPrice;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.ReferralIdService;
import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositPct;
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositPct;
import haveno.network.p2p.P2PService;
import java.math.BigInteger;
import java.util.HashMap;
@ -120,13 +121,13 @@ public class OfferUtil {
return direction == OfferDirection.BUY;
}
public long getMaxTradeLimit(PaymentAccount paymentAccount,
public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
return paymentAccount != null
return BigInteger.valueOf(paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
: 0;
: 0);
}
/**
@ -239,12 +240,12 @@ public class OfferUtil {
PaymentAccount paymentAccount,
String currencyCode) {
checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
checkArgument(securityDeposit <= getMaxSecurityDepositPct(),
"securityDeposit must not exceed " +
getMaxSecurityDepositAsPercent());
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
getMaxSecurityDepositPct());
checkArgument(securityDeposit >= getMinSecurityDepositPct(),
"securityDeposit must not be less than " +
getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
getMinSecurityDepositPct() + " but was " + securityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
@ -263,10 +264,27 @@ public class OfferUtil {
}
public static boolean isTraditionalOffer(Offer offer) {
return offer.getBaseCurrencyCode().equals("XMR");
return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode());
}
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);
}
public OpenOffer(Offer offer, long triggerPrice) {
this(offer, triggerPrice, false);
public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) {
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.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount;
this.challenge = offer.getChallenge();
this.groupId = UUID.randomUUID().toString();
this.groupId = groupId == null ? UUID.randomUUID().toString() : groupId;
state = State.PENDING;
}
@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable {
return state == State.AVAILABLE;
}
public boolean isReserved() {
return state == State.RESERVED;
}
public boolean isDeactivated() {
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.User;
import haveno.core.util.JsonUtil;
import haveno.core.util.PriceUtil;
import haveno.core.util.Validator;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.BtcWalletService;
@ -519,6 +520,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) {
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
OpenOffer sourceOffer = null;
if (sourceOfferId != null) {
@ -526,15 +533,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// get source offer
Optional<OpenOffer> sourceOfferOptional = getOpenOffer(sourceOfferId);
if (!sourceOfferOptional.isPresent()) {
errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId);
errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + ".");
return;
}
sourceOffer = sourceOfferOptional.get();
// check clone limit
int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size();
if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) {
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached.");
if (numClones >= Restrictions.getMaxOffersWithSharedFunds()) {
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.getMaxOffersWithSharedFunds() + " cloned offers with shared funds reached.");
return;
}
}
@ -632,7 +639,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void applyTriggerState(OpenOffer openOffer) {
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);
}
}
@ -661,7 +668,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) {
log.info("Canceling open offer: {}", openOffer.getId());
if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isAvailable()) {
if (isOnOfferBook(openOffer)) {
openOffer.setState(OpenOffer.State.CANCELED);
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,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
@ -692,6 +703,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
log.info("Editing open offer: {}", openOffer.getId());
offersToBeEdited.put(openOffer.getId(), openOffer);
if (openOffer.isAvailable()) {
@ -712,58 +724,73 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
Optional<OpenOffer> openOfferOptional = getOpenOffer(editedOffer.getId());
ThreadUtils.execute(() -> {
Optional<OpenOffer> openOfferOptional = getOpenOffer(editedOffer.getId());
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
openOffer.getOffer().setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
removeOpenOffer(openOffer);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer);
if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) {
if (hasConflictingClone(editedOpenOffer)) {
editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
} else {
editedOpenOffer.setState(OpenOffer.State.AVAILABLE);
}
applyTriggerState(editedOpenOffer);
} else {
if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) {
editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
} else {
editedOpenOffer.setState(originalState);
}
// check that trigger price is not set for fixed price offers
boolean isFixedPrice = editedOffer.getOfferPayload().getPrice() != 0;
if (triggerPrice != 0 && isFixedPrice) {
errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers.");
return;
}
addOpenOffer(editedOpenOffer);
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
// check for valid arbitrator signature after editing
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner());
if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) {
openOffer.getOffer().setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
removeOpenOffer(openOffer);
// reset arbitrator signature
editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer);
if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) {
if (hasConflictingClone(editedOpenOffer)) {
editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
} else {
editedOpenOffer.setState(OpenOffer.State.AVAILABLE);
}
} else {
if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) {
editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
} else {
editedOpenOffer.setState(originalState);
}
}
applyTriggerState(editedOpenOffer); // apply trigger state before adding so it's not immediately removed
addOpenOffer(editedOpenOffer);
// process offer to sign and publish
processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> {
// check for valid arbitrator signature after editing
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner());
if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) {
// reset arbitrator signature
editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
// process offer to sign and publish
synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1);
processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> {
offersToBeEdited.remove(openOffer.getId());
requestPersistence();
latch.countDown();
resultHandler.handleResult();
}, (errorMsg) -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMsg);
});
HavenoUtils.awaitLatch(latch);
}
} else {
maybeRepublishOffer(editedOpenOffer, null);
offersToBeEdited.remove(openOffer.getId());
requestPersistence();
resultHandler.handleResult();
}, (errorMsg) -> {
errorMessageHandler.handleErrorMessage(errorMsg);
});
}
} else {
maybeRepublishOffer(editedOpenOffer, null);
offersToBeEdited.remove(openOffer.getId());
requestPersistence();
resultHandler.handleResult();
errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published.");
}
} else {
errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published.");
}
}, THREAD_ID);
}
public void editOpenOfferCancel(OpenOffer openOffer,
@ -1083,6 +1110,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
try {
ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
} catch (Exception e) {
openOffer.getOffer().setState(Offer.State.INVALID);
errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
return;
}
@ -1101,17 +1129,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else {
// validate non-pending state
try {
validateSignedState(openOffer);
resultHandler.handleResult(null); // done processing if non-pending state is valid
return;
} catch (Exception e) {
log.warn(e.getMessage());
boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
if (!skipValidation) {
try {
validateSignedState(openOffer);
resultHandler.handleResult(null); // done processing if non-pending state is valid
return;
} catch (Exception e) {
log.warn(e.getMessage());
// reset arbitrator signature
openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
// reset arbitrator signature
openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
}
}
}
@ -1168,9 +1199,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer);
resultHandler.handleResult(null);
return;
}
resultHandler.handleResult(null);
return;
}
} catch (Exception e) {
if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e);
@ -1186,7 +1218,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) {
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature");
} 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)) {
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()) {
@ -1208,17 +1240,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
// check if split output tx is available for offer
if (splitOutputTx.isLocked()) return splitOutputTx;
else {
boolean isAvailable = true;
for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) {
if (output.isSpent() || output.isFrozen()) {
isAvailable = false;
break;
if (splitOutputTx != null) {
if (splitOutputTx.isLocked()) return splitOutputTx;
else {
boolean isAvailable = true;
for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) {
if (output.isSpent() || output.isFrozen()) {
isAvailable = false;
break;
}
}
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
else log.warn("Split output tx {} is no longer available for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
}
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 {} no longer exists for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
}
}
@ -1244,7 +1280,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
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>();
for (MoneroTxWallet tx : splitOutputTxs) {
if (tx.getOutputs() != null) { // outputs not available until first confirmation
@ -1267,6 +1303,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
.setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex)
.setIsIncoming(true)
.setAmount(amount)).size() > 0);
return hasExactTransfer;
}
@ -1342,7 +1379,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} catch (Exception e) {
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());
xmrWalletService.handleWalletError(e, sourceConnection);
xmrWalletService.handleWalletError(e, sourceConnection, i + 1);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -1549,8 +1586,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
// verify max length of extra info
if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().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.getMaxExtraInfoLength() + " but got " + offer.getOfferPayload().getExtraInfo().length();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
@ -1574,21 +1611,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
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
if (hasBuyerAsTakerWithoutDeposit) {
// verify maker's trade fee
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct();
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
if (offer.getMakerFeePct() != makerFeePct) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
@ -1603,8 +1633,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
// verify maker security deposit
if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
if (offer.getSellerSecurityDepositPct() != Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
@ -1619,33 +1649,43 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
} 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
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct();
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
if (offer.getMakerFeePct() != makerFeePct) {
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify taker's trade fee
if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct();
double takerFeePct = HavenoUtils.getTakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
if (offer.getTakerFeePct() != takerFeePct) {
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + takerFeePct + " but got " + offer.getTakerFeePct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify seller's security deposit
if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
if (offer.getSellerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify buyer's security deposit
if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct();
if (offer.getBuyerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getBuyerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
@ -1662,17 +1702,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// verify penalty fee
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);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT);
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT);
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), makerFeePct);
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, HavenoUtils.PENALTY_FEE_PCT);
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
offer.getId(),
penaltyFee,
@ -1696,7 +1737,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
signedOfferPayload.getPubKeyRing().hashCode(), // trader id
signedOfferPayload.getId(),
offer.getAmount().longValueExact(),
maxTradeFee.longValueExact(),
penaltyFee.longValueExact(),
request.getReserveTxHash(),
request.getReserveTxHex(),
request.getReserveTxKeyImages(),
@ -1736,6 +1777,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
errorMessage = "Exception at handleSignOfferRequest " + e.getMessage();
log.error(errorMessage + "\n", e);
} 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);
}
}
@ -1925,8 +1971,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
result,
errorMessage);
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
if (ackMessage.isSuccess()) {
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
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(
sender,
senderPubKeyRing,
@ -1952,8 +2004,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
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();
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
@ -2000,30 +2054,31 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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(),
originalOfferPayload.getDate(),
ownerNodeAddress,
originalOfferPayload.getPubKeyRing(),
originalOfferPayload.getDirection(),
originalOfferPayload.getPrice(),
normalizedPrice,
originalOfferPayload.getMarketPriceMarginPct(),
originalOfferPayload.isUseMarketBasedPrice(),
originalOfferPayload.getAmount(),
originalOfferPayload.getMinAmount(),
originalOfferPayload.getMakerFeePct(),
originalOfferPayload.getTakerFeePct(),
originalOfferPayload.getPenaltyFeePct(),
HavenoUtils.PENALTY_FEE_PCT,
originalOfferPayload.getBuyerSecurityDepositPct(),
originalOfferPayload.getSellerSecurityDepositPct(),
originalOfferPayload.getBaseCurrencyCode(),
originalOfferPayload.getCounterCurrencyCode(),
originalOffer.getBaseCurrencyCode(),
originalOffer.getCounterCurrencyCode(),
originalOfferPayload.getPaymentMethodId(),
originalOfferPayload.getMakerPaymentAccountId(),
originalOfferPayload.getCountryCode(),
originalOfferPayload.getAcceptedCountryCodes(),
originalOfferPayload.getBankId(),
originalOfferPayload.getAcceptedBankIds(),
originalOfferPayload.getVersionNr(),
Version.VERSION,
originalOfferPayload.getBlockHeightAtOfferCreation(),
originalOfferPayload.getMaxTradeLimit(),
originalOfferPayload.getMaxTradePeriod(),
@ -2047,14 +2102,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// create new offer
Offer updatedOffer = new Offer(updatedPayload);
updatedOffer.setPriceFeedService(priceFeedService);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
addOpenOffer(updatedOpenOffer);
requestPersistence();
log.info("Updating offer completed. id={}", originalOffer.getId());
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);
}
});
// add updated open offers
updatedOpenOffers.forEach(updatedOpenOffer -> {
addOpenOffer(updatedOpenOffer);
requestPersistence();
log.info("Updating offer completed. id={}", updatedOpenOffer.getId());
});
}
@ -2105,7 +2165,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ThreadUtils.execute(() -> {
// skip if prevented from publishing
if (preventedFromPublishing(openOffer)) {
if (preventedFromPublishing(openOffer, false)) {
if (completeHandler != null) completeHandler.run();
return;
}
@ -2118,7 +2178,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
latch.countDown();
// skip if prevented from publishing
if (preventedFromPublishing(openOffer)) {
if (preventedFromPublishing(openOffer, true)) {
if (completeHandler != null) completeHandler.run();
return;
}
@ -2156,11 +2216,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID);
}
private boolean preventedFromPublishing(OpenOffer openOffer) {
private boolean preventedFromPublishing(OpenOffer openOffer, boolean checkSignature) {
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true;
return openOffer.isDeactivated() ||
openOffer.isCanceled() ||
openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null ||
(checkSignature && openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) ||
hasConflictingClone(openOffer);
}
@ -2183,6 +2243,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (periodicRefreshOffersTimer == null)
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
if (!stopped) {
log.info("Refreshing my open offers");
synchronized (openOffers.getList()) {
int size = openOffers.size();
//we clone our list as openOffers might change during our delayed call
@ -2216,7 +2277,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private void maybeRefreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) {
if (preventedFromPublishing(openOffer)) return;
if (preventedFromPublishing(openOffer, true)) return;
offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(),
() -> log.debug("Successful refreshed TTL for offer"),
(errorMessage) -> {

View file

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

View file

@ -57,8 +57,15 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
// skip if reserve tx already created
if (openOffer.getReserveTxHash() != null && !openOffer.getReserveTxHash().isEmpty()) {
log.info("Reserve tx already created for offerId={}", openOffer.getShortId());
complete();
return;
// verify reserve tx key images
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) {
log.warn("Reserve tx key images missing for offerId={}", openOffer.getShortId());
setReserveTx(null);
} else {
complete();
return;
}
}
// verify monero connection
@ -72,10 +79,10 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getProtocol().startTimeoutTimer();
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
BigInteger makerFee = offer.getMaxMakerFee();
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
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();
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
@ -100,7 +107,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
throw e;
} catch (Exception e) {
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();
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
@ -115,39 +122,23 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} catch (Exception e) {
// reset state with wallet lock
setReserveTx(null);
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
offer.getOfferPayload().setReserveTxKeyImages(null);
throw e;
}
// reset protocol timeout
model.getProtocol().startTimeoutTimer();
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update offer state including clones
if (openOffer.getGroupId() == null) {
openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
} else {
for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
offerClone.setReserveTxHash(reserveTx.getHash());
offerClone.setReserveTxHex(reserveTx.getFullHex());
offerClone.setReserveTxKey(reserveTx.getKey());
offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
}
}
// update offer reserve tx
setReserveTx(reserveTx);
// reset offer funding address entries if unused
if (fundingEntry != null) {
// get reserve tx inputs
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
// collect subaddress indices of inputs
Set<Integer> inputSubaddressIndices = new HashSet<>();
@ -175,6 +166,33 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
}
}
private void setReserveTx(MoneroTxWallet reserveTx) {
OpenOffer openOffer = model.getOpenOffer();
// collect reserved key images
List<String> reservedKeyImages = null;
if (reserveTx != null) {
reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
}
// collect offers to update
List<OpenOffer> offersToUpdate = new ArrayList<OpenOffer>();
if (openOffer.getGroupId() == null) {
offersToUpdate.add(openOffer);
} else {
offersToUpdate.addAll(model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId()));
}
// update offer state
for (OpenOffer offerToUpdate : offersToUpdate) {
offerToUpdate.setReserveTxHash(reserveTx == null ? null : reserveTx.getHash());
offerToUpdate.setReserveTxHex(reserveTx == null ? null : reserveTx.getFullHex());
offerToUpdate.setReserveTxKey(reserveTx == null ? null : reserveTx.getKey());
offerToUpdate.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
}
}
private boolean isPending() {
return model.getOpenOffer().isPending();
}

View file

@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@ -60,8 +61,16 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
try {
runInterceptHook();
// create request for arbitrator to sign offer
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
// get payout address entry
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(
offer.getId(),
P2PService.getMyNodeAddress(),

View file

@ -23,6 +23,7 @@ import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.payment.PaymentAccount;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage;
import haveno.core.user.User;
@ -96,7 +97,10 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"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,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
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,
"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.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");

View file

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

View file

@ -19,6 +19,7 @@ package haveno.core.payment;
import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.BankUtil;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.AchTransferAccountPayload;
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"));
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() {
super(PaymentMethod.ACH_TRANSFER);
}
@ -79,6 +93,15 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme
@Override
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")
);
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() {
super(PaymentMethod.ALI_PAY);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -77,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount {
@Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setAccountNr(String accountNr) {

View file

@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
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
private Country country;
@ -65,7 +73,7 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
@Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public String getEmailOrMobileNr() {
@ -97,4 +105,11 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() {
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;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.InteracETransferAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.payment.validation.InteracETransferValidator;
import haveno.core.trade.HavenoUtils;
import lombok.EqualsAndHashCode;
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"));
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() {
super(PaymentMethod.INTERAC_E_TRANSFER);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,15 +62,15 @@ public final class InteracETransferAccount extends PaymentAccount {
@Override
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setEmail(String email) {
((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email);
((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email);
}
public String getEmail() {
return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail();
return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr();
}
public void setAnswer(String answer) {
@ -84,4 +96,19 @@ public final class InteracETransferAccount extends PaymentAccount {
public String 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;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import haveno.core.user.Preferences;
import haveno.core.trade.HavenoUtils;
import java.util.ArrayList;
import java.util.List;
@ -47,13 +46,6 @@ import java.util.Map;
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,
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,
// all we want is either Japanese or English here.
public static String getString(String id) {
boolean ja = userLanguage.equals("ja");
boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja");
switch (id) {
case "bank":

View file

@ -348,7 +348,7 @@ public abstract class PaymentAccount implements PersistablePayload {
if (paymentAccountPayload != null) {
String payloadJson = paymentAccountPayload.toJson();
Map<String, Object> payloadMap = gson.fromJson(payloadJson, new TypeToken<Map<String, Object>>() {}.getType());
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof List) {
@ -360,7 +360,7 @@ public abstract class PaymentAccount implements PersistablePayload {
jsonMap.putAll(payloadMap);
}
jsonMap.put("accountName", getAccountName());
jsonMap.put("accountId", getId());
if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex());
@ -435,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new LengthValidator(2, 100).validate(value));
break;
case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(2, 100).validate(value));
break;
case ANSWER:
throw new IllegalArgumentException("Not implemented");
case BANK_ACCOUNT_NAME:
@ -491,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new BICValidator().validate(value));
break;
case BRANCH_ID:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(2, 34).validate(value));
break;
case CITY:
processValidationResult(new LengthValidator(2, 34).validate(value));
break;
@ -518,7 +520,8 @@ public abstract class PaymentAccount implements PersistablePayload {
case EXTRA_INFO:
break;
case HOLDER_ADDRESS:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(0, 100).validate(value));
break;
case HOLDER_EMAIL:
throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME:
@ -616,16 +619,20 @@ public abstract class PaymentAccount implements PersistablePayload {
break;
case ACCOUNT_NR:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel("payment.accountNr");
field.setLabel(Res.get("payment.accountNr"));
break;
case ACCOUNT_OWNER:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner"));
break;
case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
field.setLabel(Res.get("payment.select.account"));
break;
case ANSWER:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.answer"));
break;
case BANK_ACCOUNT_NAME:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner"));
@ -668,11 +675,11 @@ public abstract class PaymentAccount implements PersistablePayload {
break;
case BENEFICIARY_ACCOUNT_NR:
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;
case BENEFICIARY_ADDRESS:
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;
case BENEFICIARY_CITY:
field.setComponent(PaymentAccountFormField.Component.TEXT);
@ -691,7 +698,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel("BIC");
break;
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:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.city"));
@ -717,7 +726,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.shared.optionalExtra"));
break;
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:
throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME:
@ -755,7 +766,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.swift.swiftCode.intermediary"));
break;
case MOBILE_NR:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.mobile"));
break;
case NATIONAL_ACCOUNT_ID:
throw new IllegalArgumentException("Not implemented");
case PAYID:
@ -771,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload {
case PROMPT_PAY_ID:
throw new IllegalArgumentException("Not implemented");
case QUESTION:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.secret"));
break;
case REQUIREMENTS:
throw new IllegalArgumentException("Not implemented");
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.CurrencyUtil.getTradeCurrenciesInList;
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 java.lang.String.format;
import static java.util.Arrays.stream;
@ -438,6 +439,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
// account.setSingleTradeCurrency(fiatCurrency);
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
((MoneyGramAccount) account).setCountry(country.get());
} else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) {
((AmazonGiftCardAccount) account).setCountry(country.get());
} else {
String errMsg = format("cannot set the country on a %s",
paymentAccountType.getSimpleName());

View file

@ -122,9 +122,9 @@ public class PaymentAccountUtil {
public static boolean isAmountValidForOffer(Offer offer,
PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService) {
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode());
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;
}

View file

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

View file

@ -17,12 +17,14 @@
package haveno.core.payment;
import haveno.core.api.model.PaymentAccountForm;
import haveno.core.api.model.PaymentAccountFormField;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.payment.payload.SwishAccountPayload;
import haveno.core.payment.validation.SwishValidator;
import lombok.EqualsAndHashCode;
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"));
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() {
super(PaymentMethod.SWISH);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,7 +59,7 @@ public final class SwishAccount extends PaymentAccount {
@Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setMobileNr(String mobileNr) {
@ -68,4 +77,16 @@ public final class SwishAccount extends PaymentAccount {
public String 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.locale.TraditionalCurrency;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.payload.PaymentAccountPayload;
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"));
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() {
super(PaymentMethod.TRANSFERWISE_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) {
((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address);
((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address);
}
public String getBeneficiaryAddress() {
return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress();
return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress();
}
@Override
@ -90,6 +100,13 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
@Override
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"));
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() {
super(PaymentMethod.US_POSTAL_MONEY_ORDER);
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
@ -50,7 +57,7 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount {
@Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setPostalAddress(String postalAddress) {

View file

@ -38,6 +38,13 @@ public final class WeChatPayAccount extends PaymentAccount {
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() {
super(PaymentMethod.WECHAT_PAY);
}
@ -54,7 +61,7 @@ public final class WeChatPayAccount extends PaymentAccount {
@Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setAccountNr(String accountNr) {

View file

@ -36,7 +36,7 @@ import java.util.Map;
@Getter
@Slf4j
public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String email = "";
private String emailOrMobileNr = "";
private String holderName = "";
private String question = "";
private String answer = "";
@ -52,7 +52,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
private InteracETransferAccountPayload(String paymentMethod,
String id,
String email,
String emailOrMobileNr,
String holderName,
String question,
String answer,
@ -62,7 +62,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
id,
maxTradePeriod,
excludeFromJsonDataMap);
this.email = email;
this.emailOrMobileNr = emailOrMobileNr;
this.holderName = holderName;
this.question = question;
this.answer = answer;
@ -72,7 +72,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder()
.setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder()
.setEmail(email)
.setEmailOrMobileNr(emailOrMobileNr)
.setHolderName(holderName)
.setQuestion(question)
.setAnswer(answer))
@ -82,7 +82,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
return new InteracETransferAccountPayload(proto.getPaymentMethodId(),
proto.getId(),
proto.getInteracETransferAccountPayload().getEmail(),
proto.getInteracETransferAccountPayload().getEmailOrMobileNr(),
proto.getInteracETransferAccountPayload().getHolderName(),
proto.getInteracETransferAccountPayload().getQuestion(),
proto.getInteracETransferAccountPayload().getAnswer(),
@ -98,21 +98,21 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
@Override
public String getPaymentDetails() {
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;
}
@Override
public String getPaymentDetailsForTradePopup() {
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.answer") + " " + answer;
}
@Override
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),
answer.getBytes(StandardCharsets.UTF_8))));
}

View file

@ -369,7 +369,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
CASH_APP_ID,
PAYPAL_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());
}

View file

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

View file

@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooLowPercentageValue(String input) {
try {
double percentage = ParsingUtils.parsePercentStringToDouble(input);
double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
double minPercentage = Restrictions.getMinSecurityDepositPct();
if (percentage < minPercentage)
return new ValidationResult(false,
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooHighPercentageValue(String input) {
try {
double percentage = ParsingUtils.parsePercentStringToDouble(input);
double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
double maxPercentage = Restrictions.getMaxSecurityDepositPct();
if (percentage > maxPercentage)
return new ValidationResult(false,
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(
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake
"http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck
"http://agorise7ae5g7lkqp7r7qddsyzskft7cqhgguwkadbqamtsrap5onead.onion/" // Agorise
);
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(() -> {
String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode);
String counterCurrencyCodeBase = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode);
synchronized (cache) {
if (!cache.containsKey(currencyCodeBase) || !cache.get(currencyCodeBase).isExternallyProvidedPrice()) {
cache.put(currencyCodeBase, new MarketPrice(currencyCodeBase,
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
if (!cache.containsKey(counterCurrencyCodeBase) || !cache.get(counterCurrencyCodeBase).isExternallyProvidedPrice()) {
cache.put(counterCurrencyCodeBase, new MarketPrice(counterCurrencyCodeBase,
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(counterCurrencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
0,
false));
}
@ -371,9 +371,7 @@ public class PriceFeedService {
}
/**
* Returns prices for all available currencies.
* 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).
* Returns prices for all available currencies. The base currency is always XMR.
*
* TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout
*/

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