Merge branch 'master' into master

This commit is contained in:
wowario 2025-09-19 21:37:55 +03:00 committed by GitHub
commit 03e4488bf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
482 changed files with 17598 additions and 7464 deletions

View file

@ -1,3 +1,6 @@
# GitHub Releases requires a tag, e.g:
# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1"
# git push origin 1.0.19-1
name: CI
on:
@ -11,7 +14,18 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-22.04, macos-13, windows-latest]
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-13, macos-15, windows-2025]
include:
- os: ubuntu-24.04
arch: x86_64
- os: ubuntu-24.04-arm
arch: aarch64
- os: macos-13
arch: x86_64
- os: macos-15
arch: aarch64
- os: windows-2025
arch: x86_64
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
@ -24,118 +38,196 @@ 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
- uses: actions/upload-artifact@v3
- 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:
name: error-reports-${{ matrix.os }}
path: ${{ github.workspace }}/desktop/build/reports
- name: cache nodes dependencies
uses: actions/upload-artifact@v3
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 update
sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream
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 on Unix
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
- name: Move Release Files for Linux
if: runner.os == 'Linux'
run: |
if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then
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
else
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
fi
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: runner.os == 'MacOS'
run: |
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\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows
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
- uses: actions/upload-artifact@v3
# 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
- uses: actions/upload-artifact@v3
# 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
- uses: actions/upload-artifact@v3
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@v3
- 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@v3
- 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@v3
- 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
uses: softprops/action-gh-release@v2
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
# 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
#
# git tag - lists all local tags
# git tag -d 1.0.19-1 - delete local tag
#
# git ls-remote --tags - lists all remote tags
# git push origin --delete refs/tags/1.0.19-1 - delete remote tag

View file

@ -9,7 +9,7 @@ jobs:
build:
if: github.repository == 'haveno-dex/haveno'
name: Publish coverage
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@ -19,6 +19,11 @@ jobs:
java-version: '21'
distribution: 'adopt'
- name: Create local share directory
# ubuntu-24.04 Github runners do not have `~/.local/share` directory by default.
# This causes issues when testing `FileTransferSend`
run: mkdir -p ~/.local/share
- name: Build with Gradle
run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar

View file

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

View file

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

View file

@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Copyright (C) 2020 Haveno Dex
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@ -659,4 +659,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.

View file

@ -73,6 +73,9 @@ monerod1-local:
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod2-local:
./.localnet/monerod \
@ -91,6 +94,9 @@ monerod2-local:
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
monerod3-local:
./.localnet/monerod \
@ -109,6 +115,9 @@ monerod3-local:
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections 1000 \
--max-connections-per-ip 10 \
--rpc-max-connections-per-private-ip 1000 \
#--proxy 127.0.0.1:49775 \
@ -417,12 +426,26 @@ haveno-desktop-stagenet:
--apiPort=3204 \
--useNativeXmrWallet=false \
haveno-daemon-stagenet:
./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_STAGENET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=Haveno \
--apiPassword=apitest \
--apiPort=3204 \
--useNativeXmrWallet=false \
# Mainnet network
monerod:
./.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) \
@ -468,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 \
@ -553,3 +601,19 @@ user3-desktop-mainnet:
--apiPort=1204 \
--useNativeXmrWallet=false \
--ignoreLocalXmrNode=false \
buyer-wallet-mainnet:
./.localnet/monero-wallet-rpc \
--daemon-address http://localhost:18081 \
--rpc-bind-port 18084 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
seller-wallet-mainnet:
./.localnet/monero-wallet-rpc \
--daemon-address http://localhost:18081 \
--rpc-bind-port 18085 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \

View file

@ -1,7 +1,7 @@
<div align="center">
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)](https://github.com/haveno-dex/haveno/actions)
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
[![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex)
[![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
@ -23,6 +23,10 @@ Main features:
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
## Haveno Demo
https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47
## Installing Haveno
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network.
@ -34,7 +38,7 @@ Haveno can be installed on Linux, macOS, and Windows by using a third party inst
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network.
Alternatively, you can [create your own mainnet network](create-mainnet.md).
Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md).
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
@ -69,22 +73,11 @@ If you are not able to contribute code and want to contribute development resour
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:
### Monero
<p>
<p align="center">
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
</p>
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
### Bitcoin
<p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="115" height="115"><br>
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
<code>47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB</code>
</p>

View file

@ -43,7 +43,7 @@ import java.util.stream.Collectors;
import static haveno.apitest.config.ApiTestConfig.BTC;
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
@ -158,7 +158,7 @@ public class MethodTest extends ApiTestCase {
}
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct());
if (defaultPct.precision() != 2)
throw new IllegalStateException(format(
"Unexpected decimal precision, expected 2 but actual is %d%n."

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
* {@link haveno.asset.tokens.DaiStablecoin}.
* {@link haveno.asset.tokens.DaiStablecoinERC20}.
* <p>
* The purpose of this package is to provide everything necessary for registering
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno

View file

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

View file

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

View file

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

View file

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

View file

@ -4,10 +4,16 @@
# 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
haveno.asset.tokens.DaiStablecoinERC20
haveno.asset.coins.Wownero

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,7 +49,7 @@ configure(subprojects) {
gsonVersion = '2.8.5'
guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0'
moneroJavaVersion = '0.8.33'
moneroJavaVersion = '0.8.38'
httpclient5Version = '5.0'
hamcrestVersion = '2.2'
httpclientVersion = '4.5.12'
@ -71,7 +71,7 @@ configure(subprojects) {
loggingVersion = '1.2'
lombokVersion = '1.18.30'
mockitoVersion = '5.10.0'
netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
protobufVersion = '3.19.1'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
@ -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/release4/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip',
'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888'
'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
@ -506,16 +508,16 @@ configure(project(':core')) {
} else {
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
}
}
// add the current platform's monero dependencies into the resources folder for installation
copy {
from "${monerodFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
copy {
from "${moneroRpcFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
// add the current platform's monero dependencies into the resources folder for installation
copy {
from "${monerodFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
copy {
from "${moneroRpcFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
}
@ -610,7 +612,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.14-SNAPSHOT'
version = '1.2.1-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

View file

@ -69,7 +69,7 @@ public class ClockWatcher {
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
log.info("We have been in standby mode for {} sec", missedMs / 1000);
log.warn("We have been in standby mode for {} sec", missedMs / 1000);
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
}
}

View file

@ -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.14";
public static final String VERSION = "1.2.1";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
@ -72,6 +72,25 @@ public class Version {
return false;
}
public static int compare(String version1, String version2) {
if (version1.equals(version2))
return 0;
else if (getMajorVersion(version1) > getMajorVersion(version2))
return 1;
else if (getMajorVersion(version1) < getMajorVersion(version2))
return -1;
else if (getMinorVersion(version1) > getMinorVersion(version2))
return 1;
else if (getMinorVersion(version1) < getMinorVersion(version2))
return -1;
else if (getPatchVersion(version1) > getPatchVersion(version2))
return 1;
else if (getPatchVersion(version1) < getPatchVersion(version2))
return -1;
else
return 0;
}
private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
@ -88,11 +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
public static final int TRADE_PROTOCOL_VERSION = 1;
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
// Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3
public static final int TRADE_PROTOCOL_VERSION = 3;
private static String p2pMessageVersion;
public static String getP2PMessageVersion() {

View file

@ -117,6 +117,9 @@ public class Config {
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String PASSWORD_REQUIRED = "passwordRequired";
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
public static final String DISABLE_RATE_LIMITS = "disableRateLimits";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -204,6 +207,9 @@ public class Config {
public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation;
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;
@ -621,6 +627,27 @@ public class Config {
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> updateXmrBinariesOpt =
parser.accepts(UPDATE_XMR_BINARIES,
"Update Monero binaries if applicable")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(true);
ArgumentAcceptingOptionSpec<String> xmrBlockchainPathOpt =
parser.accepts(XMR_BLOCKCHAIN_PATH,
"Path to Monero blockchain when using local Monero node")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("");
ArgumentAcceptingOptionSpec<Boolean> disableRateLimits =
parser.accepts(DISABLE_RATE_LIMITS,
"Disables all API rate limits")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(false);
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -733,6 +760,9 @@ public class Config {
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.passwordRequired = options.valueOf(passwordRequiredOpt);
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
this.disableRateLimits = options.valueOf(disableRateLimits);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
@ -742,11 +772,11 @@ public class Config {
}
// Create all appDataDir subdirectories and assign to their respective properties
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(btcNetworkDir, "keys");
this.storageDir = mkdir(btcNetworkDir, "db");
this.torDir = mkdir(btcNetworkDir, "tor");
this.walletDir = mkdir(btcNetworkDir, "wallet");
File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
this.storageDir = mkdir(xmrNetworkDir, "db");
this.torDir = mkdir(xmrNetworkDir, "tor");
this.walletDir = mkdir(xmrNetworkDir, "wallet");
// Assign values to special-case static fields
APP_DATA_DIR_VALUE = appDataDir;

View file

@ -110,7 +110,7 @@ public final class KeyRing {
* @param password The password to unlock the keys or to generate new keys, nullable.
*/
public void generateKeys(String password) {
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys");
symmetricKey = Encryption.generateSecretKey(256);
signatureKeyPair = Sig.generateKeyPair();
encryptionKeyPair = Encryption.generateKeyPair();

View file

@ -243,6 +243,11 @@ public class KeyStorage {
//noinspection ResultOfMethodCallIgnored
storageDir.mkdirs();
// password must be ascii
if (password != null && !password.matches("\\p{ASCII}*")) {
throw new IllegalArgumentException("Password must be ASCII.");
}
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
var passwordChars = password == null ? new char[0] : password.toCharArray();
try {

View file

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

View file

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

View file

@ -335,12 +335,13 @@ public class SignedWitnessService {
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
key.verifyMessage(message, signatureBase64);
verifySignatureWithECKeyResultCache.put(hash, true);
return true;
} else {
log.warn("Provided EC key is not in list of valid arbitrators.");
log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
verifySignatureWithECKeyResultCache.put(hash, false);
return false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -239,8 +239,8 @@ public class CoreApi {
xmrConnectionService.stopCheckingConnection();
}
public MoneroRpcConnection getBestAvailableXmrConnection() {
return xmrConnectionService.getBestAvailableConnection();
public MoneroRpcConnection getBestXmrConnection() {
return xmrConnectionService.getBestConnection();
}
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
@ -299,8 +299,12 @@ public class CoreApi {
return walletsService.createXmrTx(destinations);
}
public String relayXmrTx(String metadata) {
return walletsService.relayXmrTx(metadata);
public List<MoneroTxWallet> createXmrSweepTxs(String address) {
return walletsService.createXmrSweepTxs(address);
}
public List<String> relayXmrTxs(List<String> metadatas) {
return walletsService.relayXmrTxs(metadatas);
}
public long getAddressBalance(String addressString) {
@ -413,20 +417,22 @@ public class CoreApi {
}
public void postOffer(String currencyCode,
String directionAsString,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
String directionAsString,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreOffersService.postOffer(currencyCode,
directionAsString,
priceAsString,
@ -440,6 +446,8 @@ public class CoreApi {
paymentAccountId,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo,
sourceOfferId,
resultHandler,
errorMessageHandler);
}
@ -455,7 +463,8 @@ public class CoreApi {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return coreOffersService.editOffer(offerId,
currencyCode,
direction,
@ -467,7 +476,8 @@ public class CoreApi {
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
buyerAsTakerWithoutDeposit,
extraInfo);
}
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {

View file

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

View file

@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply;
import static haveno.common.util.MathUtils.roundDoubleToLong;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.monetary.CryptoMoney;
import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney;
@ -66,9 +67,7 @@ import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import static java.util.Comparator.comparing;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -124,7 +123,6 @@ public class CoreOffersService {
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
})
.collect(Collectors.toList());
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers;
}
@ -143,23 +141,20 @@ public class CoreOffersService {
}
List<OpenOffer> getMyOffers() {
List<OpenOffer> offers = openOfferManager.getOpenOffers().stream()
return openOfferManager.getOpenOffers().stream()
.filter(o -> o.getOffer().isMyOffer(keyRing))
.collect(Collectors.toList());
Set<Offer> offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images
Set<String> offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet());
return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList());
};
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
return getMyOffers().stream()
.filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
.sorted(openOfferPriceComparator(direction, CurrencyUtil.isTraditionalCurrency(currencyCode)))
.sorted(openOfferPriceComparator(direction))
.collect(Collectors.toList());
}
OpenOffer getMyOffer(String id) {
return openOfferManager.getOpenOfferById(id)
return openOfferManager.getOpenOffer(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
@ -178,15 +173,32 @@ public class CoreOffersService {
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null)
throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
// clone offer if sourceOfferId given
if (!sourceOfferId.isEmpty()) {
cloneOffer(sourceOfferId,
currencyCode,
priceAsString,
useMarketBasedPrice,
marketPriceMargin,
triggerPriceAsString,
paymentAccountId,
extraInfo,
resultHandler,
errorMessageHandler);
return;
}
// create new offer
String upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId();
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
@ -204,21 +216,75 @@ public class CoreOffersService {
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
buyerAsTakerWithoutDeposit,
extraInfo);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// We don't support atm funding from external wallet to keep it simple.
boolean useSavingsWallet = true;
//noinspection ConstantConditions
placeOffer(offer,
triggerPriceAsString,
useSavingsWallet,
true,
reserveExactAmount,
null,
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
private void cloneOffer(String sourceOfferId,
String currencyCode,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
String triggerPriceAsString,
String paymentAccountId,
String extraInfo,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
// get source offer
OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId);
Offer sourceOffer = sourceOpenOffer.getOffer();
// get trade currency (default source currency)
if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode();
if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode();
String upperCaseCurrencyCode = currencyCode.toUpperCase();
// get price (default source price)
Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString);
if (price == null) useMarketBasedPrice = true;
// get payment account
if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
// get extra info
if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
// create cloned offer
Offer offer = createOfferService.createClonedOffer(sourceOffer,
upperCaseCurrencyCode,
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
paymentAccount,
extraInfo);
// verify cloned offer
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// place offer
placeOffer(offer,
triggerPriceAsString,
true,
false, // ignored when cloning
sourceOfferId,
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
// TODO: this implementation is missing; implement.
Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
@ -230,7 +296,8 @@ public class CoreOffersService {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
return createOfferService.createAndGetOffer(offerId,
direction,
currencyCode.toUpperCase(),
@ -242,7 +309,8 @@ public class CoreOffersService {
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
buyerAsTakerWithoutDeposit,
extraInfo);
}
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -252,26 +320,6 @@ public class CoreOffersService {
// -------------------------- PRIVATE HELPERS -----------------------------
private Set<Offer> getOffersWithDuplicateKeyImages(List<Offer> offers) {
Set<Offer> duplicateFundedOffers = new HashSet<Offer>();
Set<String> seenKeyImages = new HashSet<String>();
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) {
if (offer == offer2) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
duplicateFundedOffers.add(offer2);
}
}
}
}
}
return duplicateFundedOffers;
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
@ -285,14 +333,16 @@ public class CoreOffersService {
String triggerPriceAsString,
boolean useSavingsWallet,
boolean reserveExactAmount,
String sourceOfferId,
Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode());
openOfferManager.placeOffer(offer,
useSavingsWallet,
triggerPriceAsLong,
reserveExactAmount,
true,
sourceOfferId,
resultHandler::accept,
errorMessageHandler);
}
@ -303,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;
}
@ -316,17 +365,12 @@ public class CoreOffersService {
: priceComparator.get();
}
private Comparator<OpenOffer> openOfferPriceComparator(String direction, boolean isTraditional) {
private Comparator<OpenOffer> openOfferPriceComparator(String direction) {
// A buyer probably wants to see sell orders in price ascending order.
// A seller probably wants to see buy orders in price descending order.
if (isTraditional)
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
else
return direction.equalsIgnoreCase(OfferDirection.SELL.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
}
private long priceStringToLong(String priceAsString, String currencyCode) {

View file

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

View file

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

View file

@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage;
import haveno.core.support.traderchat.TradeChatSession;
import haveno.core.support.traderchat.TraderChatManager;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Tradable;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeUtil;
@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol;
import haveno.core.trade.protocol.SellerProtocol;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.util.validation.BtcAddressValidator;
import haveno.core.xmr.model.AddressEntry;
import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT;
import haveno.core.xmr.wallet.BtcWalletService;
import static java.lang.String.format;
import java.math.BigInteger;
@ -68,7 +64,6 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bitcoinj.core.Coin;
@Singleton
@Slf4j
@ -84,7 +79,6 @@ class CoreTradesService {
private final TakeOfferModel takeOfferModel;
private final TradeManager tradeManager;
private final TraderChatManager traderChatManager;
private final TradeUtil tradeUtil;
private final OfferUtil offerUtil;
private final User user;
@ -106,7 +100,6 @@ class CoreTradesService {
this.takeOfferModel = takeOfferModel;
this.tradeManager = tradeManager;
this.traderChatManager = traderChatManager;
this.tradeUtil = tradeUtil;
this.offerUtil = offerUtil;
this.user = user;
}
@ -130,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());
}
}
@ -199,14 +189,13 @@ 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);
}
String getTradeRole(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return tradeUtil.getRole(getTrade(tradeId));
return TradeUtil.getRole(getTrade(tradeId));
}
Trade getTrade(String tradeId) {
@ -223,8 +212,7 @@ class CoreTradesService {
}
private Optional<Trade> getClosedTrade(String tradeId) {
Optional<Tradable> tradable = closedTradableManager.getTradeById(tradeId);
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
return closedTradableManager.getTradeById(tradeId);
}
List<Trade> getTrades() {
@ -267,40 +255,9 @@ class CoreTradesService {
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
}
private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
// TODO This and identical logic should be refactored into TradeUtil.
try {
return btcWalletService.getFeeEstimationTransaction(fromAddress,
toAddress,
amount,
TRADE_PAYOUT).getFee();
} catch (Exception ex) {
log.error("", ex);
throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
}
}
// Throws a RuntimeException trade is already closed.
private void verifyTradeIsNotClosed(String tradeId) {
if (getClosedTrade(tradeId).isPresent())
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
}
// Throws a RuntimeException if address is not valid.
private void verifyIsValidBTCAddress(String address) {
try {
new BtcAddressValidator().validate(address);
} catch (Throwable t) {
log.error("", t);
throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
}
}
// Throws a RuntimeException if address has a zero balance.
private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
if (fromAddressBalance.isZero())
throw new IllegalStateException(format("funds already withdrawn from address '%s'",
fromAddressEntry.getAddressString()));
}
}

View file

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

View file

@ -24,6 +24,7 @@ import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.common.config.BaseCurrencyNetwork;
import haveno.common.config.Config;
import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
import haveno.core.xmr.model.EncryptedConnectionList;
@ -32,6 +33,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener;
@ -43,7 +45,6 @@ import java.util.Set;
import org.apache.commons.lang3.exception.ExceptionUtils;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
@ -51,7 +52,6 @@ import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
@ -73,6 +73,16 @@ public final class XmrConnectionService {
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
private static final long 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 XmrConnectionFallbackType {
LOCAL,
CUSTOM,
PROVIDED
}
private final Object lock = new Object();
private final Object pollLock = new Object();
@ -86,12 +96,12 @@ public final class XmrConnectionService {
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
private final IntegerProperty numConnections = new SimpleIntegerProperty(-1);
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
private final LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener();
@Getter
private final BooleanProperty connectionServiceFallbackHandlerActive = new SimpleBooleanProperty();
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0);
@ -99,19 +109,20 @@ 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;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
private XmrKeyImagePoller keyImagePoller;
// connection switching
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
@ -120,8 +131,9 @@ public final class XmrConnectionService {
private int numRequestsLastMinute;
private long lastSwitchTimestamp;
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 60 * 1; // offer to fallback up to once every minute
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,
@ -149,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() {}
@ -173,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() {
@ -255,18 +273,29 @@ public final class XmrConnectionService {
updatePolling();
}
public MoneroRpcConnection getBestAvailableConnection() {
accountService.checkAccountOpen();
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
addLocalNodeIfIgnored(ignoredConnections);
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
public MoneroRpcConnection getBestConnection() {
return getBestConnection(new ArrayList<MoneroRpcConnection>());
}
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) {
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
accountService.checkAccountOpen();
// user needs to authorize fallback on startup after using locally synced node
if (fallbackRequiredBeforeConnectionSwitch()) {
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
return null;
}
// get best connection
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet);
return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0]));
MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections
if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0);
return bestConnection;
}
private boolean fallbackRequiredBeforeConnectionSwitch() {
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
}
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
@ -278,7 +307,7 @@ public final class XmrConnectionService {
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
return;
}
MoneroRpcConnection bestConnection = getBestAvailableConnection();
MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection);
}
@ -329,7 +358,7 @@ public final class XmrConnectionService {
if (currentConnection != null) excludedConnections.add(currentConnection);
// get connection to switch to
MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections);
MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
// remove from excluded connections after period
UserThread.runAfter(() -> {
@ -337,7 +366,7 @@ public final class XmrConnectionService {
}, EXCLUDE_CONNECTION_SECONDS);
// return if no connection to switch to
if (bestConnection == null) {
if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
log.warn("No connection to switch to");
return false;
}
@ -376,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");
}
@ -393,6 +422,17 @@ public final class XmrConnectionService {
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
}
public XmrKeyImagePoller getKeyImagePoller() {
synchronized (lock) {
if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
return keyImagePoller;
}
}
private long getKeyImageRefreshPeriodMs() {
return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
// ----------------------------- APP METHODS ------------------------------
public ReadOnlyIntegerProperty numConnectionsProperty() {
@ -408,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();
}
@ -433,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 --------------------------------
@ -478,6 +524,13 @@ public final class XmrConnectionService {
private void initialize() {
// initialize key image poller
getKeyImagePoller();
new Thread(() -> {
HavenoUtils.waitFor(20000);
keyImagePoller.poll(); // TODO: keep or remove first poll?s
}).start();
// initialize connections
initializeConnections();
@ -516,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
@ -544,8 +597,13 @@ public final class XmrConnectionService {
// update connection
if (isConnected) {
setConnection(connection.getUri());
// reset error connecting to local node
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
connectionServiceFallbackType.set(null);
}
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
}
}
@ -565,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());
@ -578,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());
@ -595,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()) {
@ -605,12 +672,9 @@ public final class XmrConnectionService {
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
// start local node if applicable
maybeStartLocalNode();
// update connection
if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection);
}
} else if (!isInitialized) {
@ -620,9 +684,6 @@ public final class XmrConnectionService {
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
connectionManager.setConnection(connection);
// start local node if applicable
maybeStartLocalNode();
}
// register connection listener
@ -635,35 +696,30 @@ public final class XmrConnectionService {
onConnectionChanged(connectionManager.getConnection());
}
private void maybeStartLocalNode() {
// skip if seed node
if (HavenoUtils.isSeedNode()) return;
// start local node if offline and used as last connection
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
try {
log.info("Starting local node");
xmrLocalNode.start();
} catch (Exception e) {
log.error("Unable to start local monero node, error={}\n", e.getMessage(), e);
}
public void startLocalNode() throws Exception {
// cannot start local node as seed node
if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node");
}
// start local node
log.info("Starting local node");
xmrLocalNode.start();
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return;
if (currentConnection == null) {
log.warn("Setting daemon connection to null");
Thread.dumpStack();
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);
@ -676,9 +732,13 @@ public final class XmrConnectionService {
numUpdates.set(numUpdates.get() + 1);
});
}
// update key image poller
keyImagePoller.setMonerod(getMonerod());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling
doPollDaemon();
doPollMonerod();
if (currentConnection != getConnection()) return; // polling can change connection
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
@ -698,67 +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();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
// poll monerod
if (monerod == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
try {
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();
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (lastInfo == null && canFallback) {
if (!connectionServiceFallbackHandlerActive.get() && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
lastFallbackInvocation = System.currentTimeMillis();
connectionServiceFallbackHandlerActive.set(true);
if (usedSyncingLocalNodeBeforeStartup) {
log.warn("Failed to fetch monerod info from local connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
} else if (isProvidedConnections()) {
log.warn("Failed to fetch monerod info from provided connections on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
} else {
log.warn("Failed to fetch monerod info from custom connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
}
}
return;
}
// 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();
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;
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
@ -766,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
@ -779,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(() -> {
@ -804,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;
}
@ -820,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) && !fallbackApplied;
return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
}
private boolean isCustomConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
}
private boolean isProvidedConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
}
}

View file

@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
import haveno.core.xmr.XmrNodeSettings;
import haveno.core.xmr.nodes.XmrNodes;
import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.wallet.XmrWalletService;
import java.io.File;
@ -55,6 +57,7 @@ public class XmrLocalNode {
private MoneroConnectionManager connectionManager;
private final Config config;
private final Preferences preferences;
private final XmrNodes xmrNodes;
private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
// required arguments
@ -69,9 +72,12 @@ public class XmrLocalNode {
}
@Inject
public XmrLocalNode(Config config, Preferences preferences) {
public XmrLocalNode(Config config,
Preferences preferences,
XmrNodes xmrNodes) {
this.config = config;
this.preferences = preferences;
this.xmrNodes = xmrNodes;
this.daemon = new MoneroDaemonRpc(getUri());
// initialize connection manager to listen to local connection
@ -101,7 +107,20 @@ public class XmrLocalNode {
* Returns whether Haveno should ignore a local Monero node even if it is usable.
*/
public boolean shouldBeIgnored() {
return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
if (config.ignoreLocalXmrNode) return true;
// ignore if fixed connection is not local
if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
// check if local node is within configuration
boolean hasConfiguredLocalNode = false;
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
hasConfiguredLocalNode = true;
break;
}
}
return !hasConfiguredLocalNode;
}
public void addListener(XmrLocalNodeListener listener) {
@ -120,7 +139,11 @@ public class XmrLocalNode {
}
public boolean equalsUri(String uri) {
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
try {
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
} catch (Exception e) {
return false;
}
}
/**
@ -166,11 +189,18 @@ public class XmrLocalNode {
var args = new ArrayList<>(MONEROD_ARGS);
var dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR;
var dataDir = "";
if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR;
}
} else {
dataDir = config.xmrBlockchainPath; // startup config overrides settings
}
if (dataDir != null && !dataDir.isEmpty()) {
args.add("--data-dir=" + dataDir);
}
if (dataDir != null) args.add("--data-dir=" + dataDir);
var bootstrapUrl = settings.getBootstrapUrl();
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {

View file

@ -80,6 +80,7 @@ public class OfferInfo implements Payload {
private final long splitOutputTxFee;
private final boolean isPrivateOffer;
private final String challenge;
private final String extraInfo;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId();
@ -115,6 +116,7 @@ public class OfferInfo implements Payload {
this.splitOutputTxFee = builder.getSplitOutputTxFee();
this.isPrivateOffer = builder.isPrivateOffer();
this.challenge = builder.getChallenge();
this.extraInfo = builder.getExtraInfo();
}
public static OfferInfo toOfferInfo(Offer offer) {
@ -127,7 +129,7 @@ public class OfferInfo implements Payload {
public static OfferInfo toMyOfferInfo(OpenOffer openOffer) {
// An OpenOffer is always my offer.
var offer = openOffer.getOffer();
var currencyCode = offer.getCurrencyCode();
var currencyCode = offer.getCounterCurrencyCode();
var isActivated = !openOffer.isDeactivated();
Optional<Price> optionalTriggerPrice = openOffer.getTriggerPrice() > 0
? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice()))
@ -148,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);
@ -184,7 +186,8 @@ public class OfferInfo implements Payload {
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
.withIsPrivateOffer(offer.isPrivateOffer())
.withChallenge(offer.getChallenge());
.withChallenge(offer.getChallenge())
.withExtraInfo(offer.getCombinedExtraInfo());
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -227,6 +230,7 @@ public class OfferInfo implements Payload {
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return builder.build();
}
@ -266,6 +270,7 @@ public class OfferInfo implements Payload {
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
.withIsPrivateOffer(proto.getIsPrivateOffer())
.withChallenge(proto.getChallenge())
.withExtraInfo(proto.getExtraInfo())
.build();
}
}

View file

@ -77,7 +77,16 @@ public final class PaymentAccountForm implements PersistablePayload {
AUSTRALIA_PAYID,
CASH_APP,
PAYPAL,
VENMO;
VENMO,
PAYSAFE,
WECHAT_PAY,
ALI_PAY,
SWISH,
TRANSFERWISE_USD,
AMAZON_GIFT_CARD,
ACH_TRANSFER,
INTERAC_E_TRANSFER,
US_POSTAL_MONEY_ORDER;
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());

View file

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

View file

@ -65,6 +65,7 @@ public final class OfferInfoBuilder {
private long splitOutputTxFee;
private boolean isPrivateOffer;
private String challenge;
private String extraInfo;
public OfferInfoBuilder withId(String id) {
this.id = id;
@ -246,6 +247,11 @@ public final class OfferInfoBuilder {
return this;
}
public OfferInfoBuilder withExtraInfo(String extraInfo) {
this.extraInfo = extraInfo;
return this;
}
public OfferInfo build() {
return new OfferInfo(this);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +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.XmrConnectionFallbackType;
import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager;
@ -158,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler;
@Setter
@Nullable
private Consumer<Boolean> displayMoneroConnectionFallbackHandler;
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter
@Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -176,7 +177,7 @@ public class HavenoSetup {
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
@Setter
@Nullable
private Runnable showPopupIfInvalidBtcConfigHandler;
private Runnable showPopupIfInvalidXmrConfigHandler;
@Setter
@Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@ -369,7 +370,7 @@ public class HavenoSetup {
// install monerod
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) {
if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) {
log.info("Installing monerod");
monerodFile.getParentFile().mkdirs();
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
@ -379,7 +380,7 @@ public class HavenoSetup {
// install monero-wallet-rpc
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) {
if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) {
log.info("Installing monero-wallet-rpc");
moneroWalletRpcFile.getParentFile().mkdirs();
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
@ -430,7 +431,7 @@ public class HavenoSetup {
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
// listen for fallback handling
getConnectionServiceFallbackHandlerActive().addListener((observable, oldValue, newValue) -> {
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionFallbackHandler == null) return;
displayMoneroConnectionFallbackHandler.accept(newValue);
});
@ -461,7 +462,7 @@ public class HavenoSetup {
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidBtcConfigHandler,
showPopupIfInvalidXmrConfigHandler,
() -> {},
() -> {});
}
@ -734,8 +735,8 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg();
}
public BooleanProperty getConnectionServiceFallbackHandlerActive() {
return xmrConnectionService.getConnectionServiceFallbackHandlerActive();
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceFallbackType();
}
public StringProperty getTopErrorMsg() {

View file

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

View file

@ -117,7 +117,7 @@ public class WalletAppSetup {
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
@Nullable Runnable showPopupIfInvalidBtcConfigHandler,
@Nullable Runnable showPopupIfInvalidXmrConfigHandler,
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
@ -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;
@ -199,8 +201,8 @@ public class WalletAppSetup {
walletInitializedHandler.run();
},
exception -> {
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
showPopupIfInvalidBtcConfigHandler.run();
if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
showPopupIfInvalidXmrConfigHandler.run();
} else {
walletServiceException.set(exception);
}

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ import static java.lang.String.format;
@Slf4j
public class CurrencyUtil {
public static void setup() {
setBaseCurrencyCode("XMR");
setBaseCurrencyCode(baseCurrencyCode);
}
private static final AssetRegistry assetRegistry = new AssetRegistry();
@ -93,7 +93,7 @@ public class CurrencyUtil {
public static Collection<TraditionalCurrency> getAllSortedFiatCurrencies(Comparator comparator) {
return getAllSortedTraditionalCurrencies(comparator).stream()
.filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode()))
.collect(Collectors.toList()); // sorted by currency name
.collect(Collectors.toList()); // sorted by currency name
}
public static List<TradeCurrency> getAllFiatCurrencies() {
@ -105,11 +105,11 @@ public class CurrencyUtil {
public static List<TradeCurrency> getAllSortedFiatCurrencies() {
return getAllSortedTraditionalCurrencies().stream()
.filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode()))
.collect(Collectors.toList()); // sorted by currency name
.collect(Collectors.toList()); // sorted by currency name
}
public static Collection<TraditionalCurrency> getAllSortedTraditionalCurrencies() {
return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name
}
public static List<TradeCurrency> getAllTraditionalCurrencies() {
@ -198,10 +198,16 @@ public class CurrencyUtil {
final List<CryptoCurrency> result = new ArrayList<>();
result.add(new CryptoCurrency("BTC", "Bitcoin"));
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("DOGE", "Dogecoin"));
result.add(new CryptoCurrency("ETH", "Ether"));
result.add(new CryptoCurrency("LTC", "Litecoin"));
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
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.add(new CryptoCurrency("WOW", "Wownero"));
result.sort(TradeCurrency::compareTo);
return result;
@ -283,7 +289,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.
@ -298,7 +304,7 @@ public class CurrencyUtil {
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
}
if (isCryptoCurrencyBase(currencyCode)) {
if (isCryptoCurrencyCodeBase(currencyCode)) {
return true;
}
@ -327,10 +333,10 @@ public class CurrencyUtil {
return isCryptoCurrency;
}
private static boolean isCryptoCurrencyBase(String currencyCode) {
private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return false;
currencyCode = currencyCode.toUpperCase();
return currencyCode.equals("USDT") || currencyCode.equals("USDC");
return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
}
public static String getCurrencyCodeBase(String currencyCode) {
@ -338,6 +344,7 @@ public class CurrencyUtil {
currencyCode = currencyCode.toUpperCase();
if (currencyCode.contains("USDT")) return "USDT";
if (currencyCode.contains("USDC")) return "USDC";
if (currencyCode.contains("DAI")) return "DAI";
return currencyCode;
}
@ -404,6 +411,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) {
@ -505,17 +519,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) {
@ -523,10 +531,7 @@ public class CurrencyUtil {
}
public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) {
if (isCryptoCurrency(currencyCode))
return Res.get(translationKey, Res.getBaseCurrencyCode(), currencyCode);
else
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
}
public static String getOfferVolumeCode(String currencyCode) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,6 @@ import com.google.inject.Singleton;
import haveno.common.app.Version;
import haveno.common.crypto.PubKeyRingProvider;
import haveno.common.util.Utilities;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.monetary.Price;
import haveno.core.payment.PaymentAccount;
@ -35,6 +34,7 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
@ -103,8 +103,9 @@ public class CreateOfferService {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
log.info("create and get offer with offerId={}, " +
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
log.info("Create and get offer with offerId={}, " +
"currencyCode={}, " +
"direction={}, " +
"fixedPrice={}, " +
@ -114,7 +115,8 @@ public class CreateOfferService {
"minAmount={}, " +
"securityDepositPct={}, " +
"isPrivateOffer={}, " +
"buyerAsTakerWithoutDeposit={}",
"buyerAsTakerWithoutDeposit={}, " +
"extraInfo={}",
offerId,
currencyCode,
direction,
@ -125,13 +127,18 @@ public class CreateOfferService {
minAmount,
securityDepositPct,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
buyerAsTakerWithoutDeposit,
extraInfo);
// verify buyer as taker security deposit
// must nullify empty string so contracts match
if ("".equals(extraInfo)) extraInfo = null;
// 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
@ -140,23 +147,19 @@ public class CreateOfferService {
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
}
long creationTime = new Date().getTime();
NodeAddress makerAddress = p2PService.getAddress();
// verify price
boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) &&
isExternalPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
// verify price
if (fixedPrice == null && !useMarketBasedPriceValue) {
throw new IllegalArgumentException("Must provide fixed price");
}
// adjust amount and min amount for fixed-price offer
if (fixedPrice != null) {
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
}
// adjust amount and min amount
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;
@ -166,20 +169,21 @@ public class CreateOfferService {
challengeHash = HavenoUtils.getChallengeHash(challenge);
}
long creationTime = new Date().getTime();
NodeAddress makerAddress = p2PService.getAddress();
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L;
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode();
String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
String baseCurrencyCode = Res.getBaseCurrencyCode();
String counterCurrencyCode = currencyCode;
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact();
boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0;
@ -201,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,
@ -216,7 +220,7 @@ public class CreateOfferService {
acceptedBanks,
Version.VERSION,
xmrWalletService.getHeight(),
maxTradeLimit,
maxTradeLimitAsLong,
maxTradePeriod,
useAutoClose,
useReOpenAfterAutoClose,
@ -228,18 +232,111 @@ public class CreateOfferService {
Version.TRADE_PROTOCOL_VERSION,
null,
null,
null);
null,
extraInfo);
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
offer.setChallenge(challenge);
return offer;
}
public Offer createClonedOffer(Offer sourceOffer,
String currencyCode,
Price fixedPrice,
boolean useMarketBasedPrice,
double marketPriceMargin,
PaymentAccount paymentAccount,
String extraInfo) {
log.info("Cloning offer with sourceId={}, " +
"currencyCode={}, " +
"fixedPrice={}, " +
"useMarketBasedPrice={}, " +
"marketPriceMargin={}, " +
"extraInfo={}",
sourceOffer.getId(),
currencyCode,
fixedPrice == null ? null : fixedPrice.getValue(),
useMarketBasedPrice,
marketPriceMargin,
extraInfo);
OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
String newOfferId = OfferUtil.getRandomOfferId();
Offer editedOffer = createAndGetOffer(newOfferId,
sourceOfferPayload.getDirection(),
currencyCode,
BigInteger.valueOf(sourceOfferPayload.getAmount()),
BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
fixedPrice,
useMarketBasedPrice,
marketPriceMargin,
sourceOfferPayload.getSellerSecurityDepositPct(),
paymentAccount,
sourceOfferPayload.isPrivateOffer(),
sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
extraInfo);
// generate one-time challenge for private offer
String challenge = null;
String challengeHash = null;
if (sourceOfferPayload.isPrivateOffer()) {
challenge = HavenoUtils.generateChallenge();
challengeHash = HavenoUtils.getChallengeHash(challenge);
}
OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
long date = new Date().getTime();
OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
date,
sourceOfferPayload.getOwnerNodeAddress(),
sourceOfferPayload.getPubKeyRing(),
sourceOfferPayload.getDirection(),
editedOfferPayload.getPrice(),
editedOfferPayload.getMarketPriceMarginPct(),
editedOfferPayload.isUseMarketBasedPrice(),
sourceOfferPayload.getAmount(),
sourceOfferPayload.getMinAmount(),
sourceOfferPayload.getMakerFeePct(),
sourceOfferPayload.getTakerFeePct(),
sourceOfferPayload.getPenaltyFeePct(),
sourceOfferPayload.getBuyerSecurityDepositPct(),
sourceOfferPayload.getSellerSecurityDepositPct(),
editedOfferPayload.getBaseCurrencyCode(),
editedOfferPayload.getCounterCurrencyCode(),
editedOfferPayload.getPaymentMethodId(),
editedOfferPayload.getMakerPaymentAccountId(),
editedOfferPayload.getCountryCode(),
editedOfferPayload.getAcceptedCountryCodes(),
editedOfferPayload.getBankId(),
editedOfferPayload.getAcceptedBankIds(),
editedOfferPayload.getVersionNr(),
sourceOfferPayload.getBlockHeightAtOfferCreation(),
editedOfferPayload.getMaxTradeLimit(),
editedOfferPayload.getMaxTradePeriod(),
sourceOfferPayload.isUseAutoClose(),
sourceOfferPayload.isUseReOpenAfterAutoClose(),
sourceOfferPayload.getLowerClosePrice(),
sourceOfferPayload.getUpperClosePrice(),
sourceOfferPayload.isPrivateOffer(),
challengeHash,
editedOfferPayload.getExtraDataMap(),
sourceOfferPayload.getProtocolVersion(),
null,
null,
sourceOfferPayload.getReserveTxKeyImages(),
editedOfferPayload.getExtraInfo());
Offer clonedOffer = new Offer(clonedOfferPayload);
clonedOffer.setPriceFeedService(priceFeedService);
clonedOffer.setChallenge(challenge);
clonedOffer.setState(Offer.State.AVAILABLE);
return clonedOffer;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isMarketPriceAvailable(String currencyCode) {
private boolean isExternalPriceAvailable(String currencyCode) {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
}

View file

@ -18,7 +18,6 @@
package haveno.core.offer;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler;
@ -40,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;
@ -174,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.");
@ -225,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.");
@ -240,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" +
@ -252,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;
@ -281,7 +278,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
}
public void setErrorMessage(String errorMessage) {
UserThread.await(() -> errorMessageProperty.set(errorMessage));
errorMessageProperty.set(errorMessage);
}
@ -386,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() {
@ -421,7 +418,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
return "";
}
public String getExtraInfo() {
public String getCombinedExtraInfo() {
StringBuilder sb = new StringBuilder();
if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) {
sb.append(getOfferExtraInfo());
}
if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) {
if (sb.length() > 0) sb.append("\n\n");
sb.append(getPaymentAccountExtraInfo());
}
return sb.toString();
}
public String getOfferExtraInfo() {
return offerPayload.getExtraInfo();
}
public String getPaymentAccountExtraInfo() {
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
@ -432,6 +445,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO);
else
return "";
}
@ -490,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() {
@ -567,21 +577,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.isUseReOpenAfterAutoClose();
}
public boolean isXmrAutoConf() {
if (!isXmr()) {
return false;
}
if (getExtraDataMap() == null || !getExtraDataMap().containsKey(OfferPayload.XMR_AUTO_CONF)) {
return false;
}
return getExtraDataMap().get(OfferPayload.XMR_AUTO_CONF).equals(OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE);
}
public boolean isXmr() {
return getCurrencyCode().equals("XMR");
}
public boolean isTraditionalOffer() {
return CurrencyUtil.isTraditionalCurrency(currencyCode);
}

View file

@ -36,6 +36,9 @@ package haveno.core.offer;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.file.JsonFileManager;
@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import haveno.network.utils.Utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import monero.daemon.model.MoneroKeyImageSpentStatus;
/**
* Handles storage and retrieval of offers.
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
* Handles validation and announcement of offers added or removed.
*/
@Slf4j
public class OfferBookService {
private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
private final P2PService p2PService;
private final PriceFeedService priceFeedService;
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
private final FilterManager filterManager;
private final JsonFileManager jsonFileManager;
private final XmrConnectionService xmrConnectionService;
// poll key images of offers
private XmrKeyImagePoller keyImagePoller;
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
private final List<Offer> validOffers = new ArrayList<Offer>();
private final List<Offer> invalidOffers = new ArrayList<Offer>();
private final Map<String, Timer> invalidOfferTimers = new HashMap<>();
public interface OfferBookChangedListener {
void onAdded(Offer offer);
void onRemoved(Offer offer);
}
@ -104,51 +113,59 @@ public class OfferBookService {
this.xmrConnectionService = xmrConnectionService;
jsonFileManager = new JsonFileManager(storageDir);
// listen for connection changes to monerod
xmrConnectionService.addConnectionListener((connection) -> {
maybeInitializeKeyImagePoller();
keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
});
// listen for offers
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
UserThread.execute(() -> {
ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
synchronized (validOffers) {
try {
validateOfferPayload(offerPayload);
replaceValidOffer(offer);
announceOfferAdded(offer);
} catch (IllegalArgumentException e) {
// ignore illegal offers
} catch (RuntimeException e) {
replaceInvalidOffer(offer); // offer can become valid later
}
}
}
});
});
}, OfferBookService.class.getSimpleName());
}
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
UserThread.execute(() -> {
ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
removeValidOffer(offerPayload.getId());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
announceOfferRemoved(offer);
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
}
});
});
}, OfferBookService.class.getSimpleName());
}
});
@ -171,6 +188,16 @@ public class OfferBookService {
}
});
}
// listen for changes to key images
xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() {
@Override
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
for (String keyImage : spentStatuses.keySet()) {
updateAffectedOffers(keyImage);
}
}
});
}
@ -178,6 +205,10 @@ public class OfferBookService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public boolean hasOffer(String offerId) {
return hasValidOffer(offerId);
}
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (filterManager.requireUpdateToNewVersionForTrading()) {
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
@ -233,21 +264,14 @@ public class OfferBookService {
}
public List<Offer> getOffers() {
return p2PService.getDataMap().values().stream()
.filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload)
.map(data -> {
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
return offer;
})
.collect(Collectors.toList());
synchronized (validOffers) {
return new ArrayList<>(validOffers);
}
}
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
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());
}
@ -266,7 +290,7 @@ public class OfferBookService {
}
public void shutDown() {
if (keyImagePoller != null) keyImagePoller.clearKeyImages();
xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
}
@ -274,37 +298,131 @@ public class OfferBookService {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private synchronized void maybeInitializeKeyImagePoller() {
if (keyImagePoller != null) return;
keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
private void announceOfferAdded(Offer offer) {
xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
}
}
// handle when key images spent
keyImagePoller.addListener(new XmrKeyImageListener() {
@Override
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
UserThread.execute(() -> {
for (String keyImage : spentStatuses.keySet()) {
updateAffectedOffers(keyImage);
}
});
private void announceOfferRemoved(Offer offer) {
updateReservedFundsSpentStatus(offer);
removeKeyImages(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
}
private boolean hasValidOffer(String offerId) {
for (Offer offer : getOffers()) {
if (offer.getId().equals(offerId)) {
return true;
}
});
// first poll after 20s
// TODO: remove?
new Thread(() -> {
HavenoUtils.waitFor(20000);
keyImagePoller.poll();
}).start();
}
return false;
}
private void replaceValidOffer(Offer offer) {
synchronized (validOffers) {
removeValidOffer(offer.getId());
validOffers.add(offer);
}
}
private long getKeyImageRefreshPeriodMs() {
return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
private void replaceInvalidOffer(Offer offer) {
synchronized (invalidOffers) {
removeInvalidOffer(offer.getId());
invalidOffers.add(offer);
// remove invalid offer after timeout
synchronized (invalidOfferTimers) {
Timer timer = invalidOfferTimers.get(offer.getId());
if (timer != null) timer.stop();
timer = UserThread.runAfter(() -> {
removeInvalidOffer(offer.getId());
}, INVALID_OFFERS_TIMEOUT);
invalidOfferTimers.put(offer.getId(), timer);
}
}
}
private void removeValidOffer(String offerId) {
synchronized (validOffers) {
validOffers.removeIf(offer -> offer.getId().equals(offerId));
}
}
private void removeInvalidOffer(String offerId) {
synchronized (invalidOffers) {
invalidOffers.removeIf(offer -> offer.getId().equals(offerId));
// remove timeout
synchronized (invalidOfferTimers) {
Timer timer = invalidOfferTimers.get(offerId);
if (timer != null) timer.stop();
invalidOfferTimers.remove(offerId);
}
}
}
private void validateOfferPayload(OfferPayload offerPayload) {
// validate offer is not banned
if (filterManager.isOfferIdBanned(offerPayload.getId())) {
throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId());
}
// validate v3 node address compliance
boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName());
if (!isV3NodeAddressCompliant) {
throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId());
}
// validate against existing offers
synchronized (validOffers) {
int numOffersWithSharedKeyImages = 0;
for (Offer offer : validOffers) {
// validate that no offer has overlapping but different key images
if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
!Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) {
throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId());
}
// validate that no offer has same key images, payment method, and currency
if (!offer.getId().equals(offerPayload.getId()) &&
offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) &&
offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) &&
offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) {
throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId());
}
// count offers with same key images
if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1);
}
// validate max offers with same key images
if (numOffersWithSharedKeyImages > Restrictions.getMaxOffersWithSharedFunds()) throw new RuntimeException("More than " + Restrictions.getMaxOffersWithSharedFunds() + " offers exist with same same key images as new offerId=" + offerPayload.getId());
}
}
private void removeKeyImages(Offer offer) {
Set<String> unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages());
synchronized (validOffers) {
for (Offer validOffer : validOffers) {
if (validOffer.getId().equals(offer.getId())) continue;
unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages());
}
}
xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName());
}
private void updateAffectedOffers(String keyImage) {
for (Offer offer : getOffers()) {
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
listener.onRemoved(offer);
@ -315,10 +433,9 @@ public class OfferBookService {
}
}
private void setReservedFundsSpent(Offer offer) {
if (keyImagePoller == null) return;
private void updateReservedFundsSpentStatus(Offer offer) {
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
offer.setReservedFundsSpent(true);
}
}
@ -328,11 +445,11 @@ public class OfferBookService {
// We filter the case that it is a MarketBasedPrice but the price is not available
// That should only be possible if the price feed provider is not available
final List<OfferForJson> offerForJsonList = getOffers().stream()
.filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCurrencyCode()) != null)
.filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCounterCurrencyCode()) != null)
.map(offer -> {
try {
return new OfferForJson(offer.getDirection(),
offer.getCurrencyCode(),
offer.getCounterCurrencyCode(),
offer.getMinAmount(),
offer.getAmount(),
offer.getPrice(),

View file

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

View file

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

View file

@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo";
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
@ -157,6 +158,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
private final boolean isPrivateOffer;
@Nullable
private final String challengeHash;
@Nullable
private final String extraInfo;
///////////////////////////////////////////////////////////////////////////////////////////
@ -200,7 +203,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
int protocolVersion,
@Nullable NodeAddress arbitratorSigner,
@Nullable byte[] arbitratorSignature,
@Nullable List<String> reserveTxKeyImages) {
@Nullable List<String> reserveTxKeyImages,
@Nullable String extraInfo) {
this.id = id;
this.date = date;
this.ownerNodeAddress = ownerNodeAddress;
@ -239,6 +243,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.upperClosePrice = upperClosePrice;
this.isPrivateOffer = isPrivateOffer;
this.challengeHash = challengeHash;
this.extraInfo = extraInfo;
}
public byte[] getHash() {
@ -289,7 +294,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
protocolVersion,
arbitratorSigner,
null,
reserveTxKeyImages
reserveTxKeyImages,
null
);
return signee.getHash();
@ -341,6 +347,10 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
}
public boolean isBuyerAsTakerWithoutDeposit() {
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@ -386,6 +396,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
}
@ -397,7 +408,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String challengeHash = ProtoUtil.stringOrNullFromProto(proto.getChallengeHash());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap();
@ -433,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getLowerClosePrice(),
proto.getUpperClosePrice(),
proto.getIsPrivateOffer(),
challengeHash,
ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
extraDataMapMap,
proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
reserveTxKeyImages);
reserveTxKeyImages,
ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
}
@Override
@ -480,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer +
",\r\n challengeHash='" + challengeHash + '\'' +
",\r\n challengeHash='" + challengeHash +
",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\r\n extraInfo='" + extraInfo +
"\r\n} ";
}
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
// The json is used for the hash in the contract and change of oder would cause a different hash and
// The json is used for the hash in the contract and change of order would cause a different hash and
// therefore a failure during trade.
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
@Override
@ -524,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
object.add("extraInfo", context.serialize(offerPayload.getExtraInfo()));
// reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts
return object;
}
}

View file

@ -37,6 +37,7 @@ import haveno.core.monetary.Volume;
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CAPABILITIES;
import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.F2F_CITY;
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
@ -48,18 +49,20 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CashAppAccount;
import haveno.core.payment.CashAtAtmAccount;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.provider.price.MarketPrice;
import haveno.core.provider.price.PriceFeedService;
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;
@ -118,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);
}
/**
@ -217,6 +220,10 @@ public class OfferUtil {
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
}
if (paymentAccount instanceof CashAtAtmAccount) {
extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo());
}
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
@ -233,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()),
@ -257,10 +264,27 @@ public class OfferUtil {
}
public static boolean isTraditionalOffer(Offer offer) {
return offer.getBaseCurrencyCode().equals("XMR");
return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode());
}
public static boolean isCryptoOffer(Offer offer) {
return offer.getCounterCurrencyCode().equals("XMR");
return CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode());
}
public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount,
String currencyCode,
OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
// disallow offers which no buyer can take due to trade limits on release
if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) {
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit));
}
if (paymentAccount != null) {
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit));
} else {
return BigInteger.ZERO;
}
}
}

View file

@ -48,6 +48,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@EqualsAndHashCode
public final class OpenOffer implements Tradable {
@ -110,19 +111,27 @@ public final class OpenOffer implements Tradable {
@Getter
@Setter
transient int numProcessingAttempts = 0;
@Getter
@Setter
private boolean deactivatedByTrigger;
@Getter
@Setter
private String groupId;
public OpenOffer(Offer offer) {
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 = groupId == null ? UUID.randomUUID().toString() : groupId;
state = State.PENDING;
}
@ -141,6 +150,8 @@ public final class OpenOffer implements Tradable {
this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey;
this.challenge = openOffer.challenge;
this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
this.groupId = openOffer.groupId;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -158,7 +169,9 @@ public final class OpenOffer implements Tradable {
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
@Nullable String reserveTxKey,
@Nullable String challenge) {
@Nullable String challenge,
boolean deactivatedByTrigger,
@Nullable String groupId) {
this.offer = offer;
this.state = state;
this.triggerPrice = triggerPrice;
@ -170,6 +183,9 @@ public final class OpenOffer implements Tradable {
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
this.challenge = challenge;
this.deactivatedByTrigger = deactivatedByTrigger;
if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19)
this.groupId = groupId;
// reset reserved state to available
if (this.state == State.RESERVED) setState(State.AVAILABLE);
@ -182,7 +198,8 @@ public final class OpenOffer implements Tradable {
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setSplitOutputTxFee(splitOutputTxFee)
.setReserveExactAmount(reserveExactAmount);
.setReserveExactAmount(reserveExactAmount)
.setDeactivatedByTrigger(deactivatedByTrigger);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
@ -191,6 +208,7 @@ public final class OpenOffer implements Tradable {
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@ -207,7 +225,9 @@ public final class OpenOffer implements Tradable {
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
proto.getDeactivatedByTrigger(),
ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
return openOffer;
}
@ -234,6 +254,14 @@ public final class OpenOffer implements Tradable {
public void setState(State state) {
this.state = state;
stateProperty.set(state);
if (state == State.AVAILABLE) {
deactivatedByTrigger = false;
}
}
public void deactivate(boolean deactivatedByTrigger) {
this.deactivatedByTrigger = deactivatedByTrigger;
setState(State.DEACTIVATED);
}
public ReadOnlyObjectProperty<State> stateProperty() {
@ -248,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;
}
@ -265,6 +297,7 @@ public final class OpenOffer implements Tradable {
",\n reserveExactAmount=" + reserveExactAmount +
",\n scheduledAmount=" + scheduledAmount +
",\n splitOutputTxFee=" + splitOutputTxFee +
",\n groupId=" + groupId +
"\n}";
}
}

File diff suppressed because it is too large Load diff

View file

@ -47,10 +47,12 @@ public final class SignedOfferList extends PersistableListAsObservable<SignedOff
@Override
public Message toProtoMessage() {
return protobuf.PersistableEnvelope.newBuilder()
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
.build();
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder()
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
.build();
}
}
public static SignedOfferList fromProto(protobuf.SignedOfferList proto) {

View file

@ -92,18 +92,17 @@ public class TriggerPriceService {
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
.filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
});
}
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null || marketPrice == null) {
return false;
}
String currencyCode = openOffer.getOffer().getCurrencyCode();
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
int smallestUnitExponent = traditionalCurrency ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
@ -117,21 +116,18 @@ 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) {
if (wasTriggered(marketPrice, openOffer)) {
String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT;
long triggerPrice = openOffer.getTriggerPrice();
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT;
if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
@ -139,24 +135,36 @@ public class TriggerPriceService {
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
);
openOfferManager.deactivateOpenOffer(openOffer, () -> {
openOfferManager.deactivateOpenOffer(openOffer, true, () -> {
}, errorMessage -> {
});
} else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) {
log.info("Market price is back within the trigger price of the open offer.\n" +
"We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
openOffer.getOffer().getShortId(),
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
);
openOfferManager.activateOpenOffer(openOffer, () -> {
}, errorMessage -> {
});
} else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
// TODO: check if open offer's reserve tx is failed or double spend seen
}
}
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);
}
@ -165,7 +173,7 @@ public class TriggerPriceService {
private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
if (openOffersByCurrency.containsKey(currencyCode)) {
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,41 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public final class AliPayAccount extends PaymentAccount {
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
new TraditionalCurrency("AED"),
new TraditionalCurrency("AUD"),
new TraditionalCurrency("CAD"),
new TraditionalCurrency("CHF"),
new TraditionalCurrency("CNY"),
new TraditionalCurrency("CZK"),
new TraditionalCurrency("DKK"),
new TraditionalCurrency("EUR"),
new TraditionalCurrency("GBP"),
new TraditionalCurrency("HKD"),
new TraditionalCurrency("IDR"),
new TraditionalCurrency("ILS"),
new TraditionalCurrency("JPY"),
new TraditionalCurrency("KRW"),
new TraditionalCurrency("LKR"),
new TraditionalCurrency("MUR"),
new TraditionalCurrency("MYR"),
new TraditionalCurrency("NOK"),
new TraditionalCurrency("NZD"),
new TraditionalCurrency("PHP"),
new TraditionalCurrency("RUB"),
new TraditionalCurrency("SEK"),
new TraditionalCurrency("SGD"),
new TraditionalCurrency("THB"),
new TraditionalCurrency("USD"),
new TraditionalCurrency("ZAR")
);
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.ACCOUNT_NR,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.SALT
);
public AliPayAccount() {
super(PaymentMethod.ALI_PAY);
@ -50,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount {
@Override
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
throw new RuntimeException("Not implemented");
return INPUT_FIELD_IDS;
}
public void setAccountNr(String accountNr) {

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,7 @@ package haveno.core.payment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.persistable.PersistablePayload;
import haveno.common.util.Utilities;
@ -341,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload {
// ---------------------------- SERIALIZATION -----------------------------
public String toJson() {
Map<String, Object> jsonMap = new HashMap<String, Object>();
if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class));
Gson gson = gsonBuilder.create();
Map<String, Object> jsonMap = new HashMap<>();
if (paymentAccountPayload != null) {
String payloadJson = paymentAccountPayload.toJson();
Map<String, Object> payloadMap = gson.fromJson(payloadJson, new TypeToken<Map<String, Object>>() {}.getType());
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof List) {
List<?> list = (List<?>) value;
String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(","));
entry.setValue(joinedString);
}
}
jsonMap.putAll(payloadMap);
}
jsonMap.put("accountName", getAccountName());
jsonMap.put("accountId", getId());
if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex());
return gsonBuilder.create().toJson(jsonMap);
return gson.toJson(jsonMap);
}
/**
@ -378,6 +396,7 @@ public abstract class PaymentAccount implements PersistablePayload {
@NonNull
public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds();
@SuppressWarnings("unchecked")
public PaymentAccountForm toForm() {
// convert to json map
@ -416,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new LengthValidator(2, 100).validate(value));
break;
case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(2, 100).validate(value));
break;
case ANSWER:
throw new IllegalArgumentException("Not implemented");
case BANK_ACCOUNT_NAME:
@ -472,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload {
processValidationResult(new BICValidator().validate(value));
break;
case BRANCH_ID:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(2, 34).validate(value));
break;
case CITY:
processValidationResult(new LengthValidator(2, 34).validate(value));
break;
@ -499,7 +520,8 @@ public abstract class PaymentAccount implements PersistablePayload {
case EXTRA_INFO:
break;
case HOLDER_ADDRESS:
throw new IllegalArgumentException("Not implemented");
processValidationResult(new LengthValidator(0, 100).validate(value));
break;
case HOLDER_EMAIL:
throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME:
@ -597,16 +619,20 @@ public abstract class PaymentAccount implements PersistablePayload {
break;
case ACCOUNT_NR:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel("payment.accountNr");
field.setLabel(Res.get("payment.accountNr"));
break;
case ACCOUNT_OWNER:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner"));
break;
case ACCOUNT_TYPE:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
field.setLabel(Res.get("payment.select.account"));
break;
case ANSWER:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.answer"));
break;
case BANK_ACCOUNT_NAME:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.owner"));
@ -649,11 +675,11 @@ public abstract class PaymentAccount implements PersistablePayload {
break;
case BENEFICIARY_ACCOUNT_NR:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.swift.account"));
field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift
break;
case BENEFICIARY_ADDRESS:
field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
field.setLabel(Res.get("payment.swift.address.beneficiary"));
field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift
break;
case BENEFICIARY_CITY:
field.setComponent(PaymentAccountFormField.Component.TEXT);
@ -672,7 +698,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel("BIC");
break;
case BRANCH_ID:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
//field.setLabel("Not implemented"); // expected to be overridden by subclasses
break;
case CITY:
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.account.city"));
@ -698,7 +726,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.shared.optionalExtra"));
break;
case HOLDER_ADDRESS:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
field.setLabel(Res.get("payment.account.owner.address"));
break;
case HOLDER_EMAIL:
throw new IllegalArgumentException("Not implemented");
case HOLDER_NAME:
@ -736,7 +766,9 @@ public abstract class PaymentAccount implements PersistablePayload {
field.setLabel(Res.get("payment.swift.swiftCode.intermediary"));
break;
case MOBILE_NR:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.mobile"));
break;
case NATIONAL_ACCOUNT_ID:
throw new IllegalArgumentException("Not implemented");
case PAYID:
@ -752,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload {
case PROMPT_PAY_ID:
throw new IllegalArgumentException("Not implemented");
case QUESTION:
throw new IllegalArgumentException("Not implemented");
field.setComponent(PaymentAccountFormField.Component.TEXT);
field.setLabel(Res.get("payment.secret"));
break;
case REQUIREMENTS:
throw new IllegalArgumentException("Not implemented");
case SALT:

View file

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

View file

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

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