diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f1751c0d36..0f51adc8cb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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
diff --git a/.github/workflows/codacy-code-reporter.yml b/.github/workflows/codacy-code-reporter.yml
index be76ef35ef..9629ea492f 100644
--- a/.github/workflows/codacy-code-reporter.yml
+++ b/.github/workflows/codacy-code-reporter.yml
@@ -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
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 7e0fefe9e7..857a27c08e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -18,7 +18,7 @@ on:
jobs:
analyze:
name: Analyze
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml
index 50ece9050c..4cd7cfa250 100644
--- a/.github/workflows/label.yml
+++ b/.github/workflows/label.yml
@@ -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
diff --git a/Makefile b/Makefile
index 8f32c3ed41..6ff271ffed 100644
--- a/Makefile
+++ b/Makefile
@@ -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 \
diff --git a/README.md b/README.md
index 4e90b06ce3..ab232aafa7 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@

- 
+ [](https://github.com/haveno-dex/haveno/actions)
[](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
[](https://twitter.com/havenodex)
[](https://matrix.to/#/#haveno:monero.social) [](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
@@ -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:
-
+

- 42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F
+ 47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB
-
-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.
diff --git a/apitest/src/test/java/haveno/apitest/method/MethodTest.java b/apitest/src/test/java/haveno/apitest/method/MethodTest.java
index 01c7a3bfd3..0007cc6c3a 100644
--- a/apitest/src/test/java/haveno/apitest/method/MethodTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/MethodTest.java
@@ -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
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."
diff --git a/assets/src/main/java/haveno/asset/CardanoAddressValidator.java b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java
new file mode 100644
index 0000000000..0b3546e4fb
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java
@@ -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 .
+ */
+
+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;
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/RippleAddressValidator.java b/assets/src/main/java/haveno/asset/RippleAddressValidator.java
new file mode 100644
index 0000000000..7325818143
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/RippleAddressValidator.java
@@ -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 .
+ */
+
+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);
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/SolanaAddressValidator.java b/assets/src/main/java/haveno/asset/SolanaAddressValidator.java
new file mode 100644
index 0000000000..92ff6468de
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/SolanaAddressValidator.java
@@ -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 .
+ */
+
+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;
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/TronAddressValidator.java b/assets/src/main/java/haveno/asset/TronAddressValidator.java
new file mode 100644
index 0000000000..6125975621
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/TronAddressValidator.java
@@ -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 .
+ */
+
+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);
+ }
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/coins/Cardano.java b/assets/src/main/java/haveno/asset/coins/Cardano.java
new file mode 100644
index 0000000000..16f2563930
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/coins/Cardano.java
@@ -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 .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.CardanoAddressValidator;
+import haveno.asset.Coin;
+
+public class Cardano extends Coin {
+
+ public Cardano() {
+ super("Cardano", "ADA", new CardanoAddressValidator());
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/coins/Dogecoin.java b/assets/src/main/java/haveno/asset/coins/Dogecoin.java
new file mode 100644
index 0000000000..f0743861ae
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/coins/Dogecoin.java
@@ -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 .
+ */
+
+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;
+ }
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/coins/Ripple.java b/assets/src/main/java/haveno/asset/coins/Ripple.java
new file mode 100644
index 0000000000..04ce84475d
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/coins/Ripple.java
@@ -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 .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.Coin;
+import haveno.asset.RippleAddressValidator;
+
+public class Ripple extends Coin {
+
+ public Ripple() {
+ super("Ripple", "XRP", new RippleAddressValidator());
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/coins/Solana.java b/assets/src/main/java/haveno/asset/coins/Solana.java
new file mode 100644
index 0000000000..e2a035601d
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/coins/Solana.java
@@ -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 .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.Coin;
+import haveno.asset.SolanaAddressValidator;
+
+public class Solana extends Coin {
+
+ public Solana() {
+ super("Solana", "SOL", new SolanaAddressValidator());
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/coins/Tron.java b/assets/src/main/java/haveno/asset/coins/Tron.java
new file mode 100644
index 0000000000..d358834eeb
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/coins/Tron.java
@@ -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 .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.Coin;
+import haveno.asset.TronAddressValidator;
+
+public class Tron extends Coin {
+
+ public Tron() {
+ super("Tron", "TRX", new TronAddressValidator());
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
index 1afb7ff1f2..ffbcac2cd3 100644
--- a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
+++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
@@ -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");
}
}
diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
index c5669d126a..c12bb37442 100644
--- a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
+++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
@@ -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");
}
}
diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
index a65c021df9..cb371bd221 100644
--- a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
+++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
@@ -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");
}
}
diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset
index 80b9cd036d..018fd76211 100644
--- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset
+++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset
@@ -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
diff --git a/assets/src/main/resources/i18n/displayStrings-assets.properties b/assets/src/main/resources/i18n/displayStrings-assets.properties
index ae23634d1c..5d67b53eab 100644
--- a/assets/src/main/resources/i18n/displayStrings-assets.properties
+++ b/assets/src/main/resources/i18n/displayStrings-assets.properties
@@ -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
diff --git a/assets/src/test/java/haveno/asset/coins/CardanoTest.java b/assets/src/test/java/haveno/asset/coins/CardanoTest.java
new file mode 100644
index 0000000000..bae8141aba
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/CardanoTest.java
@@ -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 .
+ */
+
+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");
+ }
+}
diff --git a/assets/src/test/java/haveno/asset/coins/DogecoinTest.java b/assets/src/test/java/haveno/asset/coins/DogecoinTest.java
new file mode 100644
index 0000000000..5e0e450a39
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/DogecoinTest.java
@@ -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 .
+ */
+
+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#");
+ }
+}
diff --git a/assets/src/test/java/haveno/asset/coins/RippleTest.java b/assets/src/test/java/haveno/asset/coins/RippleTest.java
new file mode 100644
index 0000000000..d984d78174
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/RippleTest.java
@@ -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 .
+ */
+
+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");
+ }
+}
diff --git a/assets/src/test/java/haveno/asset/coins/SolanaTest.java b/assets/src/test/java/haveno/asset/coins/SolanaTest.java
new file mode 100644
index 0000000000..a6ef27f770
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/SolanaTest.java
@@ -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 .
+ */
+
+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");
+ }
+}
diff --git a/assets/src/test/java/haveno/asset/coins/TronTest.java b/assets/src/test/java/haveno/asset/coins/TronTest.java
new file mode 100644
index 0000000000..dd70d2edea
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/TronTest.java
@@ -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 .
+ */
+
+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");
+ }
+}
diff --git a/build.gradle b/build.gradle
index 20ca924031..a684d51d49 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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.38'
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/release7/monero-bins-haveno-linux-x86_64.tar.gz',
+ 'linux-x86_64-sha256' : '713d64ff6423add0d065d9dfbf8a120dfbf3995d4b2093f8235b4da263d8a89c',
+ 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-aarch64.tar.gz',
+ 'linux-aarch64-sha256' : '332dcc6a5d7eec754c010a1f893f81656be1331b847b06e9be69293b456f67cc',
+ 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-mac.tar.gz',
+ 'mac-sha256' : '1c5bcd23373132528634352e604c1d732a73c634f3c77314fae503c6d23e10b0',
+ 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-windows.zip',
+ 'windows-sha256' : '3d57b980e0208a950fd795f442d9e087b5298a914b0bd96fec431188b5ab0dad'
]
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.1-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,
diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java
index 3dc04889b8..25dedd2543 100644
--- a/common/src/main/java/haveno/common/app/Version.java
+++ b/common/src/main/java/haveno/common/app/Version.java
@@ -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.1";
/**
* 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() {
diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java
index b162e211b4..03a61cbd40 100644
--- a/common/src/main/java/haveno/common/config/Config.java
+++ b/common/src/main/java/haveno/common/config/Config.java
@@ -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 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),
diff --git a/common/src/main/java/haveno/common/persistence/PersistenceManager.java b/common/src/main/java/haveno/common/persistence/PersistenceManager.java
index b9d38bd82f..88269082fb 100644
--- a/common/src/main/java/haveno/common/persistence/PersistenceManager.java
+++ b/common/src/main/java/haveno/common/persistence/PersistenceManager.java
@@ -433,12 +433,14 @@ public class PersistenceManager {
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() {
diff --git a/common/src/main/java/haveno/common/util/MathUtils.java b/common/src/main/java/haveno/common/util/MathUtils.java
index 25c91ed254..a89e4bc01e 100644
--- a/common/src/main/java/haveno/common/util/MathUtils.java
+++ b/common/src/main/java/haveno/common/util/MathUtils.java
@@ -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();
}
diff --git a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
index b4ba7b58a8..f86a0c2bb2 100644
--- a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
+++ b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
@@ -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;
}
diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
index 978b6dd715..eb36f0aa0f 100644
--- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
+++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
@@ -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);
}
diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java
index ed5645b910..cdce0e0079 100644
--- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java
+++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java
@@ -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: {}" +
diff --git a/core/src/main/java/haveno/core/alert/AlertManager.java b/core/src/main/java/haveno/core/alert/AlertManager.java
index a54f45c489..8734509ab2 100644
--- a/core/src/main/java/haveno/core/alert/AlertManager.java
+++ b/core/src/main/java/haveno/core/alert/AlertManager.java
@@ -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:
diff --git a/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java b/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java
index fd6abac552..9abc74f564 100644
--- a/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java
+++ b/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java
@@ -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:
diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java
index e8e83978eb..5162bfdb33 100644
--- a/core/src/main/java/haveno/core/api/CoreApi.java
+++ b/core/src/main/java/haveno/core/api/CoreApi.java
@@ -299,8 +299,12 @@ public class CoreApi {
return walletsService.createXmrTx(destinations);
}
- public String relayXmrTx(String metadata) {
- return walletsService.relayXmrTx(metadata);
+ public List createXmrSweepTxs(String address) {
+ return walletsService.createXmrSweepTxs(address);
+ }
+
+ public List relayXmrTxs(List metadatas) {
+ return walletsService.relayXmrTxs(metadatas);
}
public long getAddressBalance(String addressString) {
diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java
index f4bb4c803d..ef6d56472b 100644
--- a/core/src/main/java/haveno/core/api/CoreDisputesService.java
+++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java
@@ -241,12 +241,24 @@ 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 (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 (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");
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java
index 3ee7e047f1..a3b044f3b1 100644
--- a/core/src/main/java/haveno/core/api/CoreOffersService.java
+++ b/core/src/main/java/haveno/core/api/CoreOffersService.java
@@ -149,7 +149,7 @@ public class CoreOffersService {
List 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());
}
@@ -336,7 +336,7 @@ public class CoreOffersService {
String sourceOfferId,
Consumer resultHandler,
ErrorMessageHandler errorMessageHandler) {
- long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
+ long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode());
openOfferManager.placeOffer(offer,
useSavingsWallet,
triggerPriceAsLong,
@@ -353,8 +353,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 +365,12 @@ public class CoreOffersService {
: priceComparator.get();
}
- private Comparator openOfferPriceComparator(String direction, boolean isTraditional) {
+ private Comparator 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) {
diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
index b506a00ac1..b6afd098b7 100644
--- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
+++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
@@ -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) {
diff --git a/core/src/main/java/haveno/core/api/CorePriceService.java b/core/src/main/java/haveno/core/api/CorePriceService.java
index ddd194ebab..d4ed3257cf 100644
--- a/core/src/main/java/haveno/core/api/CorePriceService.java
+++ b/core/src/main/java/haveno/core/api/CorePriceService.java
@@ -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 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 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 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 sellTM = new LinkedHashMap();
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
- }
}
diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java
index 5e53ff64e4..bd3b1cc3ad 100644
--- a/core/src/main/java/haveno/core/api/CoreTradesService.java
+++ b/core/src/main/java/haveno/core/api/CoreTradesService.java
@@ -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);
}
diff --git a/core/src/main/java/haveno/core/api/CoreWalletsService.java b/core/src/main/java/haveno/core/api/CoreWalletsService.java
index 68ec8c13ea..0433a8e994 100644
--- a/core/src/main/java/haveno/core/api/CoreWalletsService.java
+++ b/core/src/main/java/haveno/core/api/CoreWalletsService.java
@@ -173,12 +173,24 @@ class CoreWalletsService {
}
}
- String relayXmrTx(String metadata) {
+ List 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 relayXmrTxs(List metadatas) {
+ accountService.checkAccountOpen();
+ verifyWalletsAreAvailable();
+ verifyEncryptedWalletIsUnlocked();
+ try {
+ return xmrWalletService.relayTxs(metadatas);
} catch (Exception ex) {
log.error("", ex);
throw new IllegalStateException(ex);
diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java
index dc38547df6..0f51f1be2b 100644
--- a/core/src/main/java/haveno/core/api/XmrConnectionService.java
+++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java
@@ -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> connections = new SimpleObjectProperty<>();
- private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
+ private final IntegerProperty numConnections = new SimpleIntegerProperty(-1);
private final ObjectProperty connectionProperty = new SimpleObjectProperty<>();
private final LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener();
@Getter
- private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>();
+ private final ObjectProperty 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 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 ignoredConnections) {
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
}
@@ -390,7 +405,7 @@ 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");
}
@@ -433,6 +448,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 +474,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 +569,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 +599,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 +623,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 +638,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 +657,11 @@ public final class XmrConnectionService {
}
}
+ // set if last node was locally syncing
+ if (!isInitialized) {
+ usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain();
+ }
+
// set connection proxies
log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@@ -666,43 +696,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 +734,11 @@ public final class XmrConnectionService {
}
// update key image poller
- keyImagePoller.setDaemon(getDaemon());
+ keyImagePoller.setMonerod(getMonerod());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling
- doPollDaemon();
+ doPollMonerod();
if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
@@ -741,74 +758,87 @@ 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(() -> pollMonerod());
+ 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() {
+ private void pollMonerod() {
if (pollInProgress) return;
- doPollDaemon();
+ doPollMonerod();
}
- private void doPollDaemon() {
+ private void doPollMonerod() {
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);
// 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 +846,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) {
+ // 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: {}", chainHeight.get(), getTargetHeight());
- lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis();
+ lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis();
}
// announce connection change if refresh period changes
@@ -829,6 +859,9 @@ public final class XmrConnectionService {
return;
}
+ // get the number of connections, which is only available if not restricted
+ int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections();
+
// update properties on user thread
UserThread.execute(() -> {
@@ -854,15 +887,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 +910,40 @@ 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);
} 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;
+ }
}
diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java
index 7295202c64..0928340d25 100644
--- a/core/src/main/java/haveno/core/api/XmrLocalNode.java
+++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java
@@ -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) {
diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java
index 76de24401a..5de9aa84a3 100644
--- a/core/src/main/java/haveno/core/api/model/OfferInfo.java
+++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java
@@ -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 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);
diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
index 6b1b494047..f7dc4a5063 100644
--- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
+++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
@@ -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());
diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java
index 8df26368ba..ab86a05240 100644
--- a/core/src/main/java/haveno/core/api/model/TradeInfo.java
+++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java
@@ -57,7 +57,7 @@ public class TradeInfo implements Payload {
private static final Function 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" +
'}';
}
}
diff --git a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java
index dcf40b8a69..ec751e0d5b 100644
--- a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java
+++ b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java
@@ -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);
}
diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
index 84bdcc746a..0bdac1abc1 100644
--- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
+++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
@@ -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));
diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java
index 19503fafd8..192e3870b7 100644
--- a/core/src/main/java/haveno/core/app/HavenoSetup.java
+++ b/core/src/main/java/haveno/core/app/HavenoSetup.java
@@ -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;
@@ -159,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler;
@Setter
@Nullable
- private Consumer displayMoneroConnectionErrorHandler;
+ private Consumer displayMoneroConnectionFallbackHandler;
@Setter
@Nullable
private Consumer 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 getConnectionServiceError() {
- return xmrConnectionService.getConnectionServiceError();
+ public ObjectProperty getConnectionServiceFallbackType() {
+ return xmrConnectionService.getConnectionServiceFallbackType();
}
public StringProperty getTopErrorMsg() {
diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
index 69609a4bba..18a26bdb5e 100644
--- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
+++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
@@ -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)
diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java
index 7d37372afd..6bf3b71f8b 100644
--- a/core/src/main/java/haveno/core/app/WalletAppSetup.java
+++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java
@@ -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;
diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
index 9c8016d506..3184d9ba11 100644
--- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
+++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
@@ -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);
diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java
index 2cebb66f3a..dd55bbc96a 100644
--- a/core/src/main/java/haveno/core/filter/FilterManager.java
+++ b/core/src/main/java/haveno/core/filter/FilterManager.java
@@ -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 filterProperty = new SimpleObjectProperty<>();
private final List listeners = new CopyOnWriteArrayList<>();
- private final List publicKeys;
private ECKey filterSigningKey;
private final Set invalidFilters = new HashSet<>();
private Consumer 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 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;
}
diff --git a/core/src/main/java/haveno/core/locale/CryptoCurrency.java b/core/src/main/java/haveno/core/locale/CryptoCurrency.java
index 6c46c9d2b3..feabaf1943 100644
--- a/core/src/main/java/haveno/core/locale/CryptoCurrency.java
+++ b/core/src/main/java/haveno/core/locale/CryptoCurrency.java
@@ -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());
}
diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java
index 6ed42b6234..7f268597be 100644
--- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java
+++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java
@@ -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 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 getAllFiatCurrencies() {
@@ -105,11 +105,11 @@ public class CurrencyUtil {
public static List 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 getAllSortedTraditionalCurrencies() {
- return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
+ return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
}
public static List getAllTraditionalCurrencies() {
@@ -198,12 +198,16 @@ public class CurrencyUtil {
final List 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) {
diff --git a/core/src/main/java/haveno/core/locale/Res.java b/core/src/main/java/haveno/core/locale/Res.java
index e44092561f..3ea4d1c5ac 100644
--- a/core/src/main/java/haveno/core/locale/Res.java
+++ b/core/src/main/java/haveno/core/locale/Res.java
@@ -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) {
diff --git a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java
index 1ab491467e..cc42342abb 100644
--- a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java
+++ b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java
@@ -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()));
}
diff --git a/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java b/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java
index 0e4777488a..257d80b515 100644
--- a/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java
+++ b/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java
@@ -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");
diff --git a/core/src/main/java/haveno/core/monetary/Price.java b/core/src/main/java/haveno/core/monetary/Price.java
index 8dccd0608d..2ea1481ee8 100644
--- a/core/src/main/java/haveno/core/monetary/Price.java
+++ b/core/src/main/java/haveno/core/monetary/Price.java
@@ -136,7 +136,7 @@ public class Price extends MonetaryWrapper implements Comparable {
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;
}
diff --git a/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java b/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java
index 976d3fa2ec..48f1eb20d5 100644
--- a/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java
+++ b/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java
@@ -15,84 +15,84 @@
* along with Bisq. If not, see .
*/
- 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);
- }
- }
-
\ No newline at end of file
+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);
+ }
+}
diff --git a/core/src/main/java/haveno/core/network/MessageState.java b/core/src/main/java/haveno/core/network/MessageState.java
index c4ae7f8f40..759b87d0a2 100644
--- a/core/src/main/java/haveno/core/network/MessageState.java
+++ b/core/src/main/java/haveno/core/network/MessageState.java
@@ -23,5 +23,6 @@ public enum MessageState {
ARRIVED,
STORED_IN_MAILBOX,
ACKNOWLEDGED,
- FAILED
+ FAILED,
+ NACKED
}
diff --git a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java
index 0dd6fe58ac..04554c7c2c 100644
--- a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java
+++ b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java
@@ -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;
diff --git a/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java
index af2434dd22..bb29a2e678 100644
--- a/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java
+++ b/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java
@@ -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);
diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java
index fab646433b..d0a79be77c 100644
--- a/core/src/main/java/haveno/core/offer/CreateOfferService.java
+++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java
@@ -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 acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List 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();
}
diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java
index 8df8511b3a..2d6c4549ff 100644
--- a/core/src/main/java/haveno/core/offer/Offer.java
+++ b/core/src/main/java/haveno/core/offer/Offer.java
@@ -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);
}
diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java
index 16faa81e57..0fde4e5031 100644
--- a/core/src/main/java/haveno/core/offer/OfferBookService.java
+++ b/core/src/main/java/haveno/core/offer/OfferBookService.java
@@ -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(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 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(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 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(),
diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java
index e64a1ee6eb..206dc4921e 100644
--- a/core/src/main/java/haveno/core/offer/OfferFilterService.java
+++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java
@@ -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 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);
}
diff --git a/core/src/main/java/haveno/core/offer/OfferForJson.java b/core/src/main/java/haveno/core/offer/OfferForJson.java
index caebcdc3dd..d96e6090d6 100644
--- a/core/src/main/java/haveno/core/offer/OfferForJson.java
+++ b/core/src/main/java/haveno/core/offer/OfferForJson.java
@@ -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();
diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java
index 2e2644630a..8792672321 100644
--- a/core/src/main/java/haveno/core/offer/OfferUtil.java
+++ b/core/src/main/java/haveno/core/offer/OfferUtil.java
@@ -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;
+ }
}
}
diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java
index f493b1b584..f5a69f7528 100644
--- a/core/src/main/java/haveno/core/offer/OpenOffer.java
+++ b/core/src/main/java/haveno/core/offer/OpenOffer.java
@@ -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;
}
diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
index e5bf40b241..22159a8e11 100644
--- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java
+++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
@@ -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 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) {
@@ -1083,6 +1094,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 +1113,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 +1183,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 +1202,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 +1224,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 +1264,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
- List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
+ List 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 removeTxs = new HashSet();
for (MoneroTxWallet tx : splitOutputTxs) {
if (tx.getOutputs() != null) { // outputs not available until first confirmation
@@ -1267,6 +1287,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
.setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex)
+ .setIsIncoming(true)
.setAmount(amount)).size() > 0);
return hasExactTransfer;
}
@@ -1342,7 +1363,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 +1570,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 +1595,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
- // verify the max version number
- if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) {
- errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION;
- log.warn(errorMessage);
- sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
- return;
- }
-
// verify maker and taker fees
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 +1617,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 +1633,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 +1686,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 +1721,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 +1761,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 +1955,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 +1988,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
private void maybeUpdatePersistedOffers() {
- List openOffersClone = getOpenOffers();
- openOffersClone.forEach(originalOpenOffer -> {
+
+ // update open offers
+ List updatedOpenOffers = new ArrayList<>();
+ getOpenOffers().forEach(originalOpenOffer -> {
Offer originalOffer = originalOpenOffer.getOffer();
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
@@ -2000,30 +2038,31 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId());
}
+ 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 +2086,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());
+ });
}
@@ -2183,6 +2227,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
diff --git a/core/src/main/java/haveno/core/offer/TriggerPriceService.java b/core/src/main/java/haveno/core/offer/TriggerPriceService.java
index 18527b774c..2b8db3d520 100644
--- a/core/src/main/java/haveno/core/offer/TriggerPriceService.java
+++ b/core/src/main/java/haveno/core/offer/TriggerPriceService.java
@@ -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 set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer);
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
index 60eaa64cdc..c0dd076232 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
@@ -72,10 +72,10 @@ public class MakerReserveOfferFunds extends Task {
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 +100,7 @@ public class MakerReserveOfferFunds extends Task {
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
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
index 3644492735..3559750830 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
@@ -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 {
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 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(),
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
index 3b1a01beeb..518ac787ad 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
@@ -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 {
/*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 {
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");
diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
index 49c6da12e9..3ff3da0b68 100644
--- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
+++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
@@ -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());
}
diff --git a/core/src/main/java/haveno/core/payment/AchTransferAccount.java b/core/src/main/java/haveno/core/payment/AchTransferAccount.java
index 9f04e4e076..8c9c1eee62 100644
--- a/core/src/main/java/haveno/core/payment/AchTransferAccount.java
+++ b/core/src/main/java/haveno/core/payment/AchTransferAccount.java
@@ -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 SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
+ private static final List 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 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;
}
}
diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java
index 1bff92b5cd..af2f617313 100644
--- a/core/src/main/java/haveno/core/payment/AliPayAccount.java
+++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java
@@ -60,6 +60,13 @@ public final class AliPayAccount extends PaymentAccount {
new TraditionalCurrency("ZAR")
);
+ private static final List 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 getInputFieldIds() {
- throw new RuntimeException("Not implemented");
+ return INPUT_FIELD_IDS;
}
public void setAccountNr(String accountNr) {
diff --git a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java
index cb3eb35c70..65cf5719ae 100644
--- a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java
+++ b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java
@@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
new TraditionalCurrency("USD")
);
+ private static final List 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 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;
+ }
}
diff --git a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java
index 579167c276..456124aac6 100644
--- a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java
+++ b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java
@@ -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 SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD"));
+ private static final List 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 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);
+ }
+
+ }
}
diff --git a/core/src/main/java/haveno/core/payment/JapanBankData.java b/core/src/main/java/haveno/core/payment/JapanBankData.java
index 9181e0f48a..c8919acec0 100644
--- a/core/src/main/java/haveno/core/payment/JapanBankData.java
+++ b/core/src/main/java/haveno/core/payment/JapanBankData.java
@@ -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":
diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java
index 14dd88482b..40351b1ca6 100644
--- a/core/src/main/java/haveno/core/payment/PaymentAccount.java
+++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java
@@ -348,7 +348,7 @@ public abstract class PaymentAccount implements PersistablePayload {
if (paymentAccountPayload != null) {
String payloadJson = paymentAccountPayload.toJson();
Map payloadMap = gson.fromJson(payloadJson, new TypeToken