mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-10-11 10:08:31 -04:00
Merge branch 'master' into master
This commit is contained in:
commit
03e4488bf9
482 changed files with 17598 additions and 7464 deletions
194
.github/workflows/build.yml
vendored
194
.github/workflows/build.yml
vendored
|
@ -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
|
||||
|
|
7
.github/workflows/codacy-code-reporter.yml
vendored
7
.github/workflows/codacy-code-reporter.yml
vendored
|
@ -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
|
||||
|
||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/label.yml
vendored
2
.github/workflows/label.yml
vendored
|
@ -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
|
||||
|
|
6
LICENSE
6
LICENSE
|
@ -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/>.
|
||||
|
|
64
Makefile
64
Makefile
|
@ -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 \
|
||||
|
|
27
README.md
27
README.md
|
@ -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">
|
||||
|
||||

|
||||
[](https://github.com/haveno-dex/haveno/actions)
|
||||
[](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
|
||||
[](https://twitter.com/havenodex)
|
||||
[](https://matrix.to/#/#haveno:monero.social) [](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
|
||||
|
@ -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>
|
||||
|
|
|
@ -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."
|
||||
|
|
106
assets/src/main/java/haveno/asset/CardanoAddressValidator.java
Normal file
106
assets/src/main/java/haveno/asset/CardanoAddressValidator.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
104
assets/src/main/java/haveno/asset/TronAddressValidator.java
Normal file
104
assets/src/main/java/haveno/asset/TronAddressValidator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
28
assets/src/main/java/haveno/asset/coins/Cardano.java
Normal file
28
assets/src/main/java/haveno/asset/coins/Cardano.java
Normal 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());
|
||||
}
|
||||
}
|
36
assets/src/main/java/haveno/asset/coins/Dogecoin.java
Normal file
36
assets/src/main/java/haveno/asset/coins/Dogecoin.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
28
assets/src/main/java/haveno/asset/coins/Ripple.java
Normal file
28
assets/src/main/java/haveno/asset/coins/Ripple.java
Normal 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());
|
||||
}
|
||||
}
|
28
assets/src/main/java/haveno/asset/coins/Solana.java
Normal file
28
assets/src/main/java/haveno/asset/coins/Solana.java
Normal 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());
|
||||
}
|
||||
}
|
28
assets/src/main/java/haveno/asset/coins/Tron.java
Normal file
28
assets/src/main/java/haveno/asset/coins/Tron.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
42
assets/src/test/java/haveno/asset/coins/CardanoTest.java
Normal file
42
assets/src/test/java/haveno/asset/coins/CardanoTest.java
Normal 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");
|
||||
}
|
||||
}
|
43
assets/src/test/java/haveno/asset/coins/DogecoinTest.java
Normal file
43
assets/src/test/java/haveno/asset/coins/DogecoinTest.java
Normal 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#");
|
||||
}
|
||||
}
|
44
assets/src/test/java/haveno/asset/coins/RippleTest.java
Normal file
44
assets/src/test/java/haveno/asset/coins/RippleTest.java
Normal 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");
|
||||
}
|
||||
}
|
46
assets/src/test/java/haveno/asset/coins/SolanaTest.java
Normal file
46
assets/src/test/java/haveno/asset/coins/SolanaTest.java
Normal 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");
|
||||
}
|
||||
}
|
45
assets/src/test/java/haveno/asset/coins/TronTest.java
Normal file
45
assets/src/test/java/haveno/asset/coins/TronTest.java
Normal 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");
|
||||
}
|
||||
}
|
44
build.gradle
44
build.gradle
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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: {}" +
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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" +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,5 +23,6 @@ public enum MessageState {
|
|||
ARRIVED,
|
||||
STORED_IN_MAILBOX,
|
||||
ACKNOWLEDGED,
|
||||
FAILED
|
||||
FAILED,
|
||||
NACKED
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -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"
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue