mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-09-22 05:54:52 -04:00
Compare commits
212 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
29b79f72fc | ||
![]() |
0a43b2b2bc | ||
![]() |
f711bd5084 | ||
![]() |
2bc877feba | ||
![]() |
e48225fec9 | ||
![]() |
414b10470a | ||
![]() |
a67e3d3dab | ||
![]() |
298f48e6f6 | ||
![]() |
eb776ea296 | ||
![]() |
6bc74d2ee8 | ||
![]() |
777cbfdc0c | ||
![]() |
812dcf27e8 | ||
![]() |
deb92b71b2 | ||
![]() |
9aca42578f | ||
![]() |
46ccb9b925 | ||
![]() |
1a911fdb9d | ||
![]() |
6a22f9c6ae | ||
![]() |
fe7b949c87 | ||
![]() |
f079ecaa82 | ||
![]() |
dd177d98cb | ||
![]() |
35418e5290 | ||
![]() |
7fa633273c | ||
![]() |
b4789ebc9e | ||
![]() |
01d56f74b3 | ||
![]() |
da14132b0e | ||
![]() |
729b5a3a74 | ||
![]() |
171acd5221 | ||
![]() |
5f3e366920 | ||
![]() |
0c1e4f31af | ||
![]() |
afe4ba3ad8 | ||
![]() |
6c89c61dd1 | ||
![]() |
d6ff436656 | ||
![]() |
cfc62aa9ae | ||
![]() |
723d5cc1dd | ||
![]() |
19ee6d4343 | ||
![]() |
55c02a53d3 | ||
![]() |
f7ec36fd70 | ||
![]() |
00737b6e79 | ||
![]() |
b0e1d5b4ce | ||
![]() |
5b08c66174 | ||
![]() |
3067609c64 | ||
![]() |
2045375f3e | ||
![]() |
348bfb7f38 | ||
![]() |
b76a556487 | ||
![]() |
4ca091940e | ||
![]() |
9e8c2cd184 | ||
![]() |
7bf9475585 | ||
![]() |
878cbb86ce | ||
![]() |
b0446c637f | ||
![]() |
0dc67f06c4 | ||
![]() |
7298a6373a | ||
![]() |
6537586976 | ||
![]() |
2844337c09 | ||
![]() |
6fd5772308 | ||
![]() |
3168c0174f | ||
![]() |
cd3fb8f6fc | ||
![]() |
4aab94ac26 | ||
![]() |
a467caa135 | ||
![]() |
5226574330 | ||
![]() |
62228625cb | ||
![]() |
d09fe78c62 | ||
![]() |
4497632e77 | ||
![]() |
c8fab5fb37 | ||
![]() |
19acab4e25 | ||
![]() |
10ef8aab2b | ||
![]() |
f96f7d2b96 | ||
![]() |
d20ad82a9f | ||
![]() |
40aacf4672 | ||
![]() |
d575f384ef | ||
![]() |
ef0f841f90 | ||
![]() |
017d8d52ba | ||
![]() |
0ba3fb02fc | ||
![]() |
2ff149b1eb | ||
![]() |
866aaac51d | ||
![]() |
4b2e294272 | ||
![]() |
6925117f12 | ||
![]() |
bf1727bda6 | ||
![]() |
1276f83485 | ||
![]() |
ffadc09712 | ||
![]() |
fd2c0f335f | ||
![]() |
3680e1d4ee | ||
![]() |
a4d744aa53 | ||
![]() |
51d40d73a7 | ||
![]() |
79dbe34359 | ||
![]() |
9f55c5d648 | ||
![]() |
f4d2646cf3 | ||
![]() |
68ee80c6ef | ||
![]() |
45e54b69e8 | ||
![]() |
7fb7504046 | ||
![]() |
f722ca4cd4 | ||
![]() |
0b04d34909 | ||
![]() |
dd1fced7fb | ||
![]() |
071659aec1 | ||
![]() |
83e1e56efb | ||
![]() |
922fb6df24 | ||
![]() |
ca1dde03f5 | ||
![]() |
ccbe2d2fe1 | ||
![]() |
b0ecc628e3 | ||
![]() |
3f5dc9c077 | ||
![]() |
12483df705 | ||
![]() |
fd664d1d30 | ||
![]() |
dff7e88428 | ||
![]() |
4627960c05 | ||
![]() |
10347ae488 | ||
![]() |
8f505ab17b | ||
![]() |
0cf34f3170 | ||
![]() |
953157c965 | ||
![]() |
68005c4daa | ||
![]() |
181bb2aa26 | ||
![]() |
a2f54215de | ||
![]() |
100b2136a7 | ||
![]() |
da9cf540e6 | ||
![]() |
19b8aaaf23 | ||
![]() |
0b4f5bf4ed | ||
![]() |
75b8eb1dc9 | ||
![]() |
384e771712 | ||
![]() |
ea506ecaf2 | ||
![]() |
ed25756ae8 | ||
![]() |
55032f94e7 | ||
![]() |
61122493b5 | ||
![]() |
d8bd91f959 | ||
![]() |
da17bcc76d | ||
![]() |
93f6337e6a | ||
![]() |
19d4df8b2e | ||
![]() |
c6ef499ced | ||
![]() |
e089a6f2a4 | ||
![]() |
5ca44c40cf | ||
![]() |
e3d7499004 | ||
![]() |
4a6043841a | ||
![]() |
9932ae84f4 | ||
![]() |
a8ba57f444 | ||
![]() |
d19610b93a | ||
![]() |
3e9a55d784 | ||
![]() |
59e509d3e3 | ||
![]() |
bd40067deb | ||
![]() |
e3e0256f7a | ||
![]() |
33c9628df3 | ||
![]() |
3546d3d931 | ||
![]() |
8cca2cbb52 | ||
![]() |
b289a256ee | ||
![]() |
5141574d70 | ||
![]() |
32148e7440 | ||
![]() |
b82d6c3004 | ||
![]() |
81f7bac452 | ||
![]() |
1875f6a70c | ||
![]() |
bc8f473b46 | ||
![]() |
2f18a74478 | ||
![]() |
ab5f6c8191 | ||
![]() |
145f0c9cf6 | ||
![]() |
4f9e39410d | ||
![]() |
e4e118f70c | ||
![]() |
53e2c5cc24 | ||
![]() |
62d5eb4bc3 | ||
![]() |
183782982c | ||
![]() |
ee49324fbb | ||
![]() |
285335d138 | ||
![]() |
aa1eb70d9a | ||
![]() |
c239f9aac0 | ||
![]() |
8ee1bb372b | ||
![]() |
ea536bb4ee | ||
![]() |
a4345ae709 | ||
![]() |
33a91cf980 | ||
![]() |
264e5f436e | ||
![]() |
501530d85f | ||
![]() |
fa375b3cbd | ||
![]() |
3648c1eb0e | ||
![]() |
98130499a7 | ||
![]() |
9dd011afc8 | ||
![]() |
5b04eb17a2 | ||
![]() |
dd65cdca13 | ||
![]() |
85ee6787cd | ||
![]() |
8f310469da | ||
![]() |
5d5eb649c6 | ||
![]() |
a37654e116 | ||
![]() |
ddab170210 | ||
![]() |
115fa96daf | ||
![]() |
4c30e4625b | ||
![]() |
050e6b907a | ||
![]() |
fe3283f3b0 | ||
![]() |
c4758d1e4b | ||
![]() |
e3946f3aba | ||
![]() |
c2fbd4b16f | ||
![]() |
c214919aa5 | ||
![]() |
81eaeb6df0 | ||
![]() |
ae5ee15a85 | ||
![]() |
0b8e43b7a8 | ||
![]() |
350aa10839 | ||
![]() |
adcd5da431 | ||
![]() |
58506b02f5 | ||
![]() |
8611593a3f | ||
![]() |
bd9c28fafa | ||
![]() |
de5250e89a | ||
![]() |
923b3ad73b | ||
![]() |
9a14d5552e | ||
![]() |
a3d3f51f02 | ||
![]() |
77429472f4 | ||
![]() |
38615edf86 | ||
![]() |
cf9a37f295 | ||
![]() |
c7a3a9740f | ||
![]() |
13e13d945d | ||
![]() |
bfef0f9492 | ||
![]() |
39909e7936 | ||
![]() |
695f2b8dd3 | ||
![]() |
8f778be4d9 | ||
![]() |
821ef16d8f | ||
![]() |
58590d60df | ||
![]() |
8eccbcce43 | ||
![]() |
bbfc5d5fed | ||
![]() |
22db354cb2 | ||
![]() |
60ceff6695 | ||
![]() |
c87b8a5b45 | ||
![]() |
bf055556f1 |
390 changed files with 9301 additions and 4732 deletions
144
.github/workflows/build.yml
vendored
144
.github/workflows/build.yml
vendored
|
@ -14,7 +14,18 @@ jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-22.04, macos-13, windows-latest]
|
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-15, windows-2025]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-24.04
|
||||||
|
arch: x86_64
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
arch: aarch64
|
||||||
|
- os: macos-15-intel
|
||||||
|
arch: x86_64
|
||||||
|
- os: macos-15
|
||||||
|
arch: aarch64
|
||||||
|
- os: windows-2025
|
||||||
|
arch: x86_64
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
@ -27,8 +38,25 @@ jobs:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Build with Gradle
|
- name: Create local share directory
|
||||||
|
# ubuntu-24.04 Github runners do not have `~/.local/share` directory by default.
|
||||||
|
# This causes issues when testing `FileTransferSend`
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: mkdir -p ~/.local/share
|
||||||
|
- name: Build with Gradle with tests
|
||||||
|
if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }}
|
||||||
run: ./gradlew build --stacktrace --scan
|
run: ./gradlew build --stacktrace --scan
|
||||||
|
- name: Build with Gradle, skip Desktop tests
|
||||||
|
# JavaFX `21.x.x` ships with `x86_64` versions of
|
||||||
|
# `libprism_es2.so` and `libprism_s2.so` shared objects.
|
||||||
|
# This causes desktop tests to fail on `linux/aarch64`
|
||||||
|
if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }}
|
||||||
|
run: |
|
||||||
|
./gradlew build --stacktrace --scan -x desktop:test
|
||||||
|
echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \
|
||||||
|
intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \
|
||||||
|
shared objects, causing tests to fail. \
|
||||||
|
This should be revisited when JavaFX is next updated."
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
|
@ -38,115 +66,131 @@ jobs:
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
name: cached-localnet
|
name: cached-localnet-${{ matrix.os }}
|
||||||
path: .localnet
|
path: .localnet
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
|
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
|
||||||
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
- name: Install WiX Toolset
|
- name: Install WiX Toolset
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe
|
Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe
|
||||||
.\wix314.exe /quiet /norestart
|
.\wix314.exe /quiet /norestart
|
||||||
shell: powershell
|
shell: powershell
|
||||||
- name: Build Haveno Installer
|
- name: Build Haveno Installer with tests
|
||||||
|
if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }}
|
||||||
|
run: ./gradlew clean build --refresh-keys --refresh-dependencies
|
||||||
|
working-directory: .
|
||||||
|
- name: Build Haveno Installer, skip Desktop tests
|
||||||
|
# JavaFX `21.x.x` ships with `x86_64` versions of
|
||||||
|
# `libprism_es2.so` and `libprism_s2.so` shared objects.
|
||||||
|
# This causes desktop tests to fail on `linux/aarch64`
|
||||||
|
if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }}
|
||||||
run: |
|
run: |
|
||||||
./gradlew clean build --refresh-keys --refresh-dependencies
|
./gradlew clean build --refresh-keys --refresh-dependencies -x desktop:test
|
||||||
./gradlew packageInstallers
|
echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \
|
||||||
|
intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \
|
||||||
|
shared objects, causing tests to fail. \
|
||||||
|
This should be revisited when JavaFX is next updated."
|
||||||
|
working-directory: .
|
||||||
|
- name: Package Haveno Installer
|
||||||
|
run: ./gradlew packageInstallers
|
||||||
working-directory: .
|
working-directory: .
|
||||||
|
|
||||||
# get version from jar
|
# get version from jar
|
||||||
- name: Set Version Unix
|
- name: Set Version Unix
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
|
if: runner.os != 'Windows'
|
||||||
run: |
|
run: |
|
||||||
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
|
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
- name: Set Version Windows
|
- name: Set Version Windows
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
|
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
|
||||||
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
- name: Move Release Files for Linux
|
- name: Move Release Files for Linux
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
mkdir ${{ github.workspace }}/release-linux-rpm
|
mkdir ${{ github.workspace }}/release-linux-rpm
|
||||||
mkdir ${{ github.workspace }}/release-linux-deb
|
mkdir ${{ github.workspace }}/release-linux-deb
|
||||||
mkdir ${{ github.workspace }}/release-linux-flatpak
|
mkdir ${{ github.workspace }}/release-linux-flatpak
|
||||||
mkdir ${{ github.workspace }}/release-linux-appimage
|
mkdir ${{ github.workspace }}/release-linux-appimage
|
||||||
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.rpm
|
||||||
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.deb
|
||||||
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.flatpak
|
||||||
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.AppImage
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Move Release Files for macOS
|
- name: Move Release Files for macOS
|
||||||
if: ${{ matrix.os == 'macos-13' }}
|
if: runner.os == 'MacOS'
|
||||||
run: |
|
run: |
|
||||||
mkdir ${{ github.workspace }}/release-macos
|
mkdir ${{ github.workspace }}/release-macos-${{ matrix.arch }}
|
||||||
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos-${{ matrix.arch }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-installer.dmg
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos-${{ matrix.arch }}
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Move Release Files on Windows
|
- name: Move Release Files on Windows
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
mkdir ${{ github.workspace }}/release-windows
|
mkdir ${{ github.workspace }}/release-windows
|
||||||
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
|
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-${{ matrix.arch }}-installer.exe
|
||||||
Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows
|
Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows
|
||||||
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
# win
|
# Windows artifacts
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Windows artifacts"
|
name: "Windows artifacts"
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: runner.os == 'Windows'
|
||||||
with:
|
with:
|
||||||
name: haveno-windows
|
name: haveno-windows-${{ matrix.arch }}
|
||||||
path: ${{ github.workspace }}/release-windows
|
path: ${{ github.workspace }}/release-windows
|
||||||
# macos
|
|
||||||
|
# macOS artifacts
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "macOS artifacts"
|
name: "macOS artifacts"
|
||||||
if: ${{ matrix.os == 'macos-13' }}
|
if: runner.os == 'MacOS'
|
||||||
with:
|
with:
|
||||||
name: haveno-macos
|
name: haveno-macos-${{ matrix.arch }}
|
||||||
path: ${{ github.workspace }}/release-macos
|
path: ${{ github.workspace }}/release-macos-${{ matrix.arch }}
|
||||||
# linux
|
|
||||||
|
# Linux artifacts
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - deb artifact"
|
name: "Linux - deb artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-deb
|
name: haveno-linux-${{ matrix.arch }}-deb
|
||||||
path: ${{ github.workspace }}/release-linux-deb
|
path: ${{ github.workspace }}/release-linux-deb
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - rpm artifact"
|
name: "Linux - rpm artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-rpm
|
name: haveno-linux-${{ matrix.arch }}-rpm
|
||||||
path: ${{ github.workspace }}/release-linux-rpm
|
path: ${{ github.workspace }}/release-linux-rpm
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - AppImage artifact"
|
name: "Linux - AppImage artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-appimage
|
name: haveno-linux-${{ matrix.arch }}-appimage
|
||||||
path: ${{ github.workspace }}/release-linux-appimage
|
path: ${{ github.workspace }}/release-linux-appimage
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - flatpak artifact"
|
name: "Linux - flatpak artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-flatpak
|
name: haveno-linux-${{ matrix.arch }}-flatpak
|
||||||
path: ${{ github.workspace }}/release-linux-flatpak
|
path: ${{ github.workspace }}/release-linux-flatpak
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
@ -154,14 +198,30 @@ jobs:
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
# Linux x86_64
|
||||||
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
||||||
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
||||||
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
||||||
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
||||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||||
${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
|
||||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
|
# Linux aarch64
|
||||||
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
|
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-aarch64-installer.deb
|
||||||
|
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-aarch64-installer.rpm
|
||||||
|
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-aarch64.AppImage
|
||||||
|
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-aarch64.flatpak
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-aarch64-SNAPSHOT-all.jar.SHA-256
|
||||||
|
|
||||||
|
# macOS x86_64
|
||||||
|
${{ github.workspace }}/release-macos-x86_64/haveno-v${{ env.VERSION }}-macos-x86_64-installer.dmg
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||||
|
|
||||||
|
# macOS aarch64
|
||||||
|
${{ github.workspace }}/release-macos-aarch64/haveno-v${{ env.VERSION }}-macos-aarch64-installer.dmg
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-aarch64-SNAPSHOT-all.jar.SHA-256
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-x86_64-installer.exe
|
||||||
${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
||||||
|
|
||||||
# https://git-scm.com/docs/git-tag - git-tag Docu
|
# https://git-scm.com/docs/git-tag - git-tag Docu
|
||||||
|
|
7
.github/workflows/codacy-code-reporter.yml
vendored
7
.github/workflows/codacy-code-reporter.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
||||||
build:
|
build:
|
||||||
if: github.repository == 'haveno-dex/haveno'
|
if: github.repository == 'haveno-dex/haveno'
|
||||||
name: Publish coverage
|
name: Publish coverage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
@ -19,6 +19,11 @@ jobs:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
|
||||||
|
- name: Create local share directory
|
||||||
|
# ubuntu-24.04 Github runners do not have `~/.local/share` directory by default.
|
||||||
|
# This causes issues when testing `FileTransferSend`
|
||||||
|
run: mkdir -p ~/.local/share
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar
|
run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar
|
||||||
|
|
||||||
|
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -18,7 +18,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|
2
.github/workflows/label.yml
vendored
2
.github/workflows/label.yml
vendored
|
@ -7,7 +7,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
issueLabeled:
|
issueLabeled:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Bounty explanation
|
- name: Bounty explanation
|
||||||
uses: peter-evans/create-or-update-comment@v3
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
|
|
59
Makefile
59
Makefile
|
@ -70,11 +70,12 @@ monerod1-local:
|
||||||
--log-level 0 \
|
--log-level 0 \
|
||||||
--add-exclusive-node 127.0.0.1:48080 \
|
--add-exclusive-node 127.0.0.1:48080 \
|
||||||
--add-exclusive-node 127.0.0.1:58080 \
|
--add-exclusive-node 127.0.0.1:58080 \
|
||||||
--max-connections-per-ip 10 \
|
|
||||||
--rpc-access-control-origins http://localhost:8080 \
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
--fixed-difficulty 500 \
|
--fixed-difficulty 500 \
|
||||||
--disable-rpc-ban \
|
--disable-rpc-ban \
|
||||||
--rpc-max-connections-per-private-ip 100 \
|
--rpc-max-connections 1000 \
|
||||||
|
--max-connections-per-ip 10 \
|
||||||
|
--rpc-max-connections-per-private-ip 1000 \
|
||||||
|
|
||||||
monerod2-local:
|
monerod2-local:
|
||||||
./.localnet/monerod \
|
./.localnet/monerod \
|
||||||
|
@ -90,11 +91,12 @@ monerod2-local:
|
||||||
--confirm-external-bind \
|
--confirm-external-bind \
|
||||||
--add-exclusive-node 127.0.0.1:28080 \
|
--add-exclusive-node 127.0.0.1:28080 \
|
||||||
--add-exclusive-node 127.0.0.1:58080 \
|
--add-exclusive-node 127.0.0.1:58080 \
|
||||||
--max-connections-per-ip 10 \
|
|
||||||
--rpc-access-control-origins http://localhost:8080 \
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
--fixed-difficulty 500 \
|
--fixed-difficulty 500 \
|
||||||
--disable-rpc-ban \
|
--disable-rpc-ban \
|
||||||
--rpc-max-connections-per-private-ip 100 \
|
--rpc-max-connections 1000 \
|
||||||
|
--max-connections-per-ip 10 \
|
||||||
|
--rpc-max-connections-per-private-ip 1000 \
|
||||||
|
|
||||||
monerod3-local:
|
monerod3-local:
|
||||||
./.localnet/monerod \
|
./.localnet/monerod \
|
||||||
|
@ -110,11 +112,12 @@ monerod3-local:
|
||||||
--confirm-external-bind \
|
--confirm-external-bind \
|
||||||
--add-exclusive-node 127.0.0.1:28080 \
|
--add-exclusive-node 127.0.0.1:28080 \
|
||||||
--add-exclusive-node 127.0.0.1:48080 \
|
--add-exclusive-node 127.0.0.1:48080 \
|
||||||
--max-connections-per-ip 10 \
|
|
||||||
--rpc-access-control-origins http://localhost:8080 \
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
--fixed-difficulty 500 \
|
--fixed-difficulty 500 \
|
||||||
--disable-rpc-ban \
|
--disable-rpc-ban \
|
||||||
--rpc-max-connections-per-private-ip 100 \
|
--rpc-max-connections 1000 \
|
||||||
|
--max-connections-per-ip 10 \
|
||||||
|
--rpc-max-connections-per-private-ip 1000 \
|
||||||
|
|
||||||
#--proxy 127.0.0.1:49775 \
|
#--proxy 127.0.0.1:49775 \
|
||||||
|
|
||||||
|
@ -440,6 +443,9 @@ monerod:
|
||||||
./.localnet/monerod \
|
./.localnet/monerod \
|
||||||
--bootstrap-daemon-address auto \
|
--bootstrap-daemon-address auto \
|
||||||
--rpc-access-control-origins http://localhost:8080 \
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
|
--rpc-max-connections 1000 \
|
||||||
|
--max-connections-per-ip 10 \
|
||||||
|
--rpc-max-connections-per-private-ip 1000 \
|
||||||
|
|
||||||
seednode:
|
seednode:
|
||||||
./haveno-seednode$(APP_EXT) \
|
./haveno-seednode$(APP_EXT) \
|
||||||
|
@ -485,6 +491,31 @@ arbitrator-desktop-mainnet:
|
||||||
--xmrNode=http://127.0.0.1:18081 \
|
--xmrNode=http://127.0.0.1:18081 \
|
||||||
--useNativeXmrWallet=false \
|
--useNativeXmrWallet=false \
|
||||||
|
|
||||||
|
arbitrator2-daemon-mainnet:
|
||||||
|
./haveno-daemon$(APP_EXT) \
|
||||||
|
--baseCurrencyNetwork=XMR_MAINNET \
|
||||||
|
--useLocalhostForP2P=false \
|
||||||
|
--useDevPrivilegeKeys=false \
|
||||||
|
--nodePort=9999 \
|
||||||
|
--appName=haveno-XMR_MAINNET_arbitrator2 \
|
||||||
|
--apiPassword=apitest \
|
||||||
|
--apiPort=1205 \
|
||||||
|
--passwordRequired=false \
|
||||||
|
--xmrNode=http://127.0.0.1:18081 \
|
||||||
|
--useNativeXmrWallet=false \
|
||||||
|
|
||||||
|
arbitrator2-desktop-mainnet:
|
||||||
|
./haveno-desktop$(APP_EXT) \
|
||||||
|
--baseCurrencyNetwork=XMR_MAINNET \
|
||||||
|
--useLocalhostForP2P=false \
|
||||||
|
--useDevPrivilegeKeys=false \
|
||||||
|
--nodePort=9999 \
|
||||||
|
--appName=haveno-XMR_MAINNET_arbitrator2 \
|
||||||
|
--apiPassword=apitest \
|
||||||
|
--apiPort=1205 \
|
||||||
|
--xmrNode=http://127.0.0.1:18081 \
|
||||||
|
--useNativeXmrWallet=false \
|
||||||
|
|
||||||
haveno-daemon-mainnet:
|
haveno-daemon-mainnet:
|
||||||
./haveno-daemon$(APP_EXT) \
|
./haveno-daemon$(APP_EXT) \
|
||||||
--baseCurrencyNetwork=XMR_MAINNET \
|
--baseCurrencyNetwork=XMR_MAINNET \
|
||||||
|
@ -570,3 +601,19 @@ user3-desktop-mainnet:
|
||||||
--apiPort=1204 \
|
--apiPort=1204 \
|
||||||
--useNativeXmrWallet=false \
|
--useNativeXmrWallet=false \
|
||||||
--ignoreLocalXmrNode=false \
|
--ignoreLocalXmrNode=false \
|
||||||
|
|
||||||
|
buyer-wallet-mainnet:
|
||||||
|
./.localnet/monero-wallet-rpc \
|
||||||
|
--daemon-address http://localhost:18081 \
|
||||||
|
--rpc-bind-port 18084 \
|
||||||
|
--rpc-login rpc_user:abc123 \
|
||||||
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
|
--wallet-dir ./.localnet \
|
||||||
|
|
||||||
|
seller-wallet-mainnet:
|
||||||
|
./.localnet/monero-wallet-rpc \
|
||||||
|
--daemon-address http://localhost:18081 \
|
||||||
|
--rpc-bind-port 18085 \
|
||||||
|
--rpc-login rpc_user:abc123 \
|
||||||
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
|
--wallet-dir ./.localnet \
|
||||||
|
|
14
README.md
14
README.md
|
@ -1,7 +1,7 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
|
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
|
||||||
|
|
||||||

|
[](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://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
|
||||||
[](https://twitter.com/havenodex)
|
[](https://twitter.com/havenodex)
|
||||||
[](https://matrix.to/#/#haveno:monero.social) [](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
|
[](https://matrix.to/#/#haveno:monero.social) [](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
|
||||||
|
@ -67,19 +67,17 @@ See the [developer guide](docs/developer-guide.md) to get started developing for
|
||||||
|
|
||||||
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides.
|
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides.
|
||||||
|
|
||||||
If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties.
|
If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties.
|
||||||
|
|
||||||
## Bounties
|
## Bounties
|
||||||
|
|
||||||
To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
|
To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
|
||||||
|
|
||||||
## Support and sponsorships
|
## Support
|
||||||
|
|
||||||
To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project:
|
To bring Haveno to life, we need resources. If you have the possibility, please consider donating to the project:
|
||||||
|
|
||||||
<p>
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
|
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
|
||||||
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
|
<code>47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ import java.util.stream.Collectors;
|
||||||
import static haveno.apitest.config.ApiTestConfig.BTC;
|
import static haveno.apitest.config.ApiTestConfig.BTC;
|
||||||
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
|
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
|
||||||
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
|
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
|
||||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
|
@ -158,7 +158,7 @@ public class MethodTest extends ApiTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
|
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
|
||||||
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
|
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct());
|
||||||
if (defaultPct.precision() != 2)
|
if (defaultPct.precision() != 2)
|
||||||
throw new IllegalStateException(format(
|
throw new IllegalStateException(format(
|
||||||
"Unexpected decimal precision, expected 2 but actual is %d%n."
|
"Unexpected decimal precision, expected 2 but actual is %d%n."
|
||||||
|
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,6 @@ public class TetherUSDERC20 extends Erc20Token {
|
||||||
public TetherUSDERC20() {
|
public TetherUSDERC20() {
|
||||||
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
||||||
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
||||||
super("Tether USD (ERC20)", "USDT-ERC20");
|
super("Tether USD", "USDT-ERC20");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ public class TetherUSDTRC20 extends Trc20Token {
|
||||||
public TetherUSDTRC20() {
|
public TetherUSDTRC20() {
|
||||||
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
|
||||||
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
|
||||||
super("Tether USD (TRC20)", "USDT-TRC20");
|
super("Tether USD", "USDT-TRC20");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,6 @@ import haveno.asset.Erc20Token;
|
||||||
public class USDCoinERC20 extends Erc20Token {
|
public class USDCoinERC20 extends Erc20Token {
|
||||||
|
|
||||||
public USDCoinERC20() {
|
public USDCoinERC20() {
|
||||||
super("USD Coin (ERC20)", "USDC-ERC20");
|
super("USD Coin", "USDC-ERC20");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,14 @@
|
||||||
# See https://haveno.exchange/list-asset for complete instructions.
|
# See https://haveno.exchange/list-asset for complete instructions.
|
||||||
haveno.asset.coins.Bitcoin$Mainnet
|
haveno.asset.coins.Bitcoin$Mainnet
|
||||||
haveno.asset.coins.BitcoinCash
|
haveno.asset.coins.BitcoinCash
|
||||||
|
haveno.asset.coins.Cardano
|
||||||
|
haveno.asset.coins.Dogecoin
|
||||||
haveno.asset.coins.Ether
|
haveno.asset.coins.Ether
|
||||||
haveno.asset.coins.Litecoin
|
haveno.asset.coins.Litecoin
|
||||||
haveno.asset.coins.Monero
|
haveno.asset.coins.Monero
|
||||||
|
haveno.asset.coins.Ripple
|
||||||
|
haveno.asset.coins.Solana
|
||||||
|
haveno.asset.coins.Tron
|
||||||
haveno.asset.tokens.TetherUSDERC20
|
haveno.asset.tokens.TetherUSDERC20
|
||||||
haveno.asset.tokens.TetherUSDTRC20
|
haveno.asset.tokens.TetherUSDTRC20
|
||||||
haveno.asset.tokens.USDCoinERC20
|
haveno.asset.tokens.USDCoinERC20
|
||||||
|
|
|
@ -4,11 +4,6 @@
|
||||||
# E.g.: [main-view].[component].[description]
|
# E.g.: [main-view].[component].[description]
|
||||||
# In some cases we use enum values or constants to map to display strings
|
# In some cases we use enum values or constants to map to display strings
|
||||||
|
|
||||||
# A annoying issue with property files is that we need to use 2 single quotes in display string
|
|
||||||
# containing variables (e.g. {0}), otherwise the variable will not be resolved.
|
|
||||||
# In display string which do not use a variable a single quote is ok.
|
|
||||||
# E.g. Don''t .... {1}
|
|
||||||
|
|
||||||
# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces
|
# We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces
|
||||||
# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose!
|
# at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose!
|
||||||
# To make longer strings with better readable you can make a line break with \ which does not result in a line break
|
# To make longer strings with better readable you can make a line break with \ which does not result in a line break
|
||||||
|
|
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");
|
||||||
|
}
|
||||||
|
}
|
24
build.gradle
24
build.gradle
|
@ -49,7 +49,7 @@ configure(subprojects) {
|
||||||
gsonVersion = '2.8.5'
|
gsonVersion = '2.8.5'
|
||||||
guavaVersion = '32.1.1-jre'
|
guavaVersion = '32.1.1-jre'
|
||||||
guiceVersion = '7.0.0'
|
guiceVersion = '7.0.0'
|
||||||
moneroJavaVersion = '0.8.36'
|
moneroJavaVersion = '0.8.38'
|
||||||
httpclient5Version = '5.0'
|
httpclient5Version = '5.0'
|
||||||
hamcrestVersion = '2.2'
|
hamcrestVersion = '2.2'
|
||||||
httpclientVersion = '4.5.12'
|
httpclientVersion = '4.5.12'
|
||||||
|
@ -79,7 +79,9 @@ configure(subprojects) {
|
||||||
slf4jVersion = '1.7.30'
|
slf4jVersion = '1.7.30'
|
||||||
sparkVersion = '2.5.2'
|
sparkVersion = '2.5.2'
|
||||||
|
|
||||||
os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
|
def osName = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
|
||||||
|
def osArch = System.getProperty("os.arch").toLowerCase()
|
||||||
|
os = (osName == 'mac' && (osArch.contains('aarch64') || osArch.contains('arm'))) ? 'mac-aarch64' : osName
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -457,14 +459,14 @@ configure(project(':core')) {
|
||||||
doLast {
|
doLast {
|
||||||
// get monero binaries download url
|
// get monero binaries download url
|
||||||
Map moneroBinaries = [
|
Map moneroBinaries = [
|
||||||
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz',
|
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-x86_64.tar.gz',
|
||||||
'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
|
'linux-x86_64-sha256' : '713d64ff6423add0d065d9dfbf8a120dfbf3995d4b2093f8235b4da263d8a89c',
|
||||||
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
|
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-aarch64.tar.gz',
|
||||||
'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
|
'linux-aarch64-sha256' : '332dcc6a5d7eec754c010a1f893f81656be1331b847b06e9be69293b456f67cc',
|
||||||
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
|
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-mac.tar.gz',
|
||||||
'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
|
'mac-sha256' : '1c5bcd23373132528634352e604c1d732a73c634f3c77314fae503c6d23e10b0',
|
||||||
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
|
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-windows.zip',
|
||||||
'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
|
'windows-sha256' : '3d57b980e0208a950fd795f442d9e087b5298a914b0bd96fec431188b5ab0dad'
|
||||||
]
|
]
|
||||||
|
|
||||||
String osKey
|
String osKey
|
||||||
|
@ -610,7 +612,7 @@ configure(project(':desktop')) {
|
||||||
apply plugin: 'com.github.johnrengelman.shadow'
|
apply plugin: 'com.github.johnrengelman.shadow'
|
||||||
apply from: 'package/package.gradle'
|
apply from: 'package/package.gradle'
|
||||||
|
|
||||||
version = '1.0.19-SNAPSHOT'
|
version = '1.2.1-SNAPSHOT'
|
||||||
|
|
||||||
jar.manifest.attributes(
|
jar.manifest.attributes(
|
||||||
"Implementation-Title": project.name,
|
"Implementation-Title": project.name,
|
||||||
|
|
|
@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||||
public class Version {
|
public class Version {
|
||||||
// The application versions
|
// The application versions
|
||||||
// We use semantic versioning with major, minor and patch
|
// We use semantic versioning with major, minor and patch
|
||||||
public static final String VERSION = "1.0.19";
|
public static final String VERSION = "1.2.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds a list of the tagged resource files for optimizing the getData requests.
|
* Holds a list of the tagged resource files for optimizing the getData requests.
|
||||||
|
@ -107,12 +107,11 @@ public class Version {
|
||||||
|
|
||||||
// The version no. of the current protocol. The offer holds that version.
|
// The version no. of the current protocol. The offer holds that version.
|
||||||
// A taker will check the version of the offers to see if his version is compatible.
|
// A taker will check the version of the offers to see if his version is compatible.
|
||||||
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
|
|
||||||
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
|
|
||||||
// the Haveno app.
|
// the Haveno app.
|
||||||
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
|
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
|
||||||
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
|
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
|
||||||
public static final int TRADE_PROTOCOL_VERSION = 2;
|
// Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3
|
||||||
|
public static final int TRADE_PROTOCOL_VERSION = 3;
|
||||||
private static String p2pMessageVersion;
|
private static String p2pMessageVersion;
|
||||||
|
|
||||||
public static String getP2PMessageVersion() {
|
public static String getP2PMessageVersion() {
|
||||||
|
|
|
@ -119,6 +119,7 @@ public class Config {
|
||||||
public static final String PASSWORD_REQUIRED = "passwordRequired";
|
public static final String PASSWORD_REQUIRED = "passwordRequired";
|
||||||
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
|
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
|
||||||
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
|
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
|
||||||
|
public static final String DISABLE_RATE_LIMITS = "disableRateLimits";
|
||||||
|
|
||||||
// Default values for certain options
|
// Default values for certain options
|
||||||
public static final int UNSPECIFIED_PORT = -1;
|
public static final int UNSPECIFIED_PORT = -1;
|
||||||
|
@ -208,6 +209,7 @@ public class Config {
|
||||||
public final boolean passwordRequired;
|
public final boolean passwordRequired;
|
||||||
public final boolean updateXmrBinaries;
|
public final boolean updateXmrBinaries;
|
||||||
public final String xmrBlockchainPath;
|
public final String xmrBlockchainPath;
|
||||||
|
public final boolean disableRateLimits;
|
||||||
|
|
||||||
// Properties derived from options but not exposed as options themselves
|
// Properties derived from options but not exposed as options themselves
|
||||||
public final File torDir;
|
public final File torDir;
|
||||||
|
@ -639,6 +641,13 @@ public class Config {
|
||||||
.ofType(String.class)
|
.ofType(String.class)
|
||||||
.defaultsTo("");
|
.defaultsTo("");
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<Boolean> disableRateLimits =
|
||||||
|
parser.accepts(DISABLE_RATE_LIMITS,
|
||||||
|
"Disables all API rate limits")
|
||||||
|
.withRequiredArg()
|
||||||
|
.ofType(boolean.class)
|
||||||
|
.defaultsTo(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CompositeOptionSet options = new CompositeOptionSet();
|
CompositeOptionSet options = new CompositeOptionSet();
|
||||||
|
|
||||||
|
@ -753,6 +762,7 @@ public class Config {
|
||||||
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
||||||
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
|
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
|
||||||
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
|
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
|
||||||
|
this.disableRateLimits = options.valueOf(disableRateLimits);
|
||||||
} catch (OptionException ex) {
|
} catch (OptionException ex) {
|
||||||
throw new ConfigException("problem parsing option '%s': %s",
|
throw new ConfigException("problem parsing option '%s': %s",
|
||||||
ex.options().get(0),
|
ex.options().get(0),
|
||||||
|
|
|
@ -433,12 +433,14 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||||
private void maybeStartTimerForPersistence() {
|
private void maybeStartTimerForPersistence() {
|
||||||
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
|
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
|
||||||
// can be rather long.
|
// can be rather long.
|
||||||
|
UserThread.execute(() -> {
|
||||||
if (timer == null) {
|
if (timer == null) {
|
||||||
timer = UserThread.runAfter(() -> {
|
timer = UserThread.runAfter(() -> {
|
||||||
persistNow(null);
|
persistNow(null);
|
||||||
UserThread.execute(() -> timer = null);
|
UserThread.execute(() -> timer = null);
|
||||||
}, source.delay, TimeUnit.MILLISECONDS);
|
}, source.delay, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forcePersistNow() {
|
public void forcePersistNow() {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
@ -85,6 +86,11 @@ public class MathUtils {
|
||||||
return ((double) value) * factor;
|
return ((double) value) * factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BigInteger scaleUpByPowerOf10(BigInteger value, int exponent) {
|
||||||
|
BigInteger factor = BigInteger.TEN.pow(exponent);
|
||||||
|
return value.multiply(factor);
|
||||||
|
}
|
||||||
|
|
||||||
public static double scaleDownByPowerOf10(double value, int exponent) {
|
public static double scaleDownByPowerOf10(double value, int exponent) {
|
||||||
double factor = Math.pow(10, exponent);
|
double factor = Math.pow(10, exponent);
|
||||||
return value / factor;
|
return value / factor;
|
||||||
|
@ -95,6 +101,11 @@ public class MathUtils {
|
||||||
return ((double) value) / factor;
|
return ((double) value) / factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BigInteger scaleDownByPowerOf10(BigInteger value, int exponent) {
|
||||||
|
BigInteger factor = BigInteger.TEN.pow(exponent);
|
||||||
|
return value.divide(factor);
|
||||||
|
}
|
||||||
|
|
||||||
public static double exactMultiply(double value1, double value2) {
|
public static double exactMultiply(double value1, double value2) {
|
||||||
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
|
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,12 +335,13 @@ public class SignedWitnessService {
|
||||||
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
|
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
|
||||||
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
|
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
|
||||||
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
|
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
|
||||||
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
|
String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
|
||||||
|
if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
|
||||||
key.verifyMessage(message, signatureBase64);
|
key.verifyMessage(message, signatureBase64);
|
||||||
verifySignatureWithECKeyResultCache.put(hash, true);
|
verifySignatureWithECKeyResultCache.put(hash, true);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.warn("Provided EC key is not in list of valid arbitrators.");
|
log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
|
||||||
verifySignatureWithECKeyResultCache.put(hash, false);
|
verifySignatureWithECKeyResultCache.put(hash, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -654,7 +654,7 @@ public class AccountAgeWitnessService {
|
||||||
Date peersCurrentDate,
|
Date peersCurrentDate,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
checkNotNull(offer);
|
checkNotNull(offer);
|
||||||
final String currencyCode = offer.getCurrencyCode();
|
final String currencyCode = offer.getCounterCurrencyCode();
|
||||||
final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode);
|
final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode);
|
||||||
BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit;
|
BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit;
|
||||||
if (!hasTradeLimitException(peersWitness)) {
|
if (!hasTradeLimitException(peersWitness)) {
|
||||||
|
@ -673,7 +673,7 @@ public class AccountAgeWitnessService {
|
||||||
"\nPeers trade limit=" + peersCurrentTradeLimit +
|
"\nPeers trade limit=" + peersCurrentTradeLimit +
|
||||||
"\nOffer ID=" + offer.getShortId() +
|
"\nOffer ID=" + offer.getShortId() +
|
||||||
"\nPaymentMethod=" + offer.getPaymentMethod().getId() +
|
"\nPaymentMethod=" + offer.getPaymentMethod().getId() +
|
||||||
"\nCurrencyCode=" + offer.getCurrencyCode();
|
"\nCurrencyCode=" + offer.getCounterCurrencyCode();
|
||||||
log.warn(msg);
|
log.warn(msg);
|
||||||
errorMessageHandler.handleErrorMessage(msg);
|
errorMessageHandler.handleErrorMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ public class AccountAgeWitnessUtils {
|
||||||
boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) &&
|
boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) &&
|
||||||
!accountAgeWitnessService.peerHasSignedWitness(trade) &&
|
!accountAgeWitnessService.peerHasSignedWitness(trade) &&
|
||||||
accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount());
|
accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount());
|
||||||
log.info("AccountSigning debug log: " +
|
log.debug("AccountSigning debug log: " +
|
||||||
"\ntradeId: {}" +
|
"\ntradeId: {}" +
|
||||||
"\nis buyer: {}" +
|
"\nis buyer: {}" +
|
||||||
"\nbuyer account age witness info: {}" +
|
"\nbuyer account age witness info: {}" +
|
||||||
|
|
|
@ -105,9 +105,9 @@ public class AlertManager {
|
||||||
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
|
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
|
||||||
case XMR_STAGENET:
|
case XMR_STAGENET:
|
||||||
return List.of(
|
return List.of(
|
||||||
"036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f",
|
"03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
|
||||||
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854",
|
"02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
|
||||||
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36");
|
"0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
|
||||||
case XMR_MAINNET:
|
case XMR_MAINNET:
|
||||||
return List.of();
|
return List.of();
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -104,9 +104,9 @@ public class PrivateNotificationManager implements MessageListener {
|
||||||
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
|
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
|
||||||
case XMR_STAGENET:
|
case XMR_STAGENET:
|
||||||
return List.of(
|
return List.of(
|
||||||
"02ba7c5de295adfe57b60029f3637a2c6b1d0e969a8aaefb9e0ddc3a7963f26925",
|
"03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
|
||||||
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854",
|
"02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
|
||||||
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36");
|
"0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
|
||||||
case XMR_MAINNET:
|
case XMR_MAINNET:
|
||||||
return List.of();
|
return List.of();
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -299,8 +299,12 @@ public class CoreApi {
|
||||||
return walletsService.createXmrTx(destinations);
|
return walletsService.createXmrTx(destinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String relayXmrTx(String metadata) {
|
public List<MoneroTxWallet> createXmrSweepTxs(String address) {
|
||||||
return walletsService.relayXmrTx(metadata);
|
return walletsService.createXmrSweepTxs(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> relayXmrTxs(List<String> metadatas) {
|
||||||
|
return walletsService.relayXmrTxs(metadatas);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getAddressBalance(String addressString) {
|
public long getAddressBalance(String addressString) {
|
||||||
|
|
|
@ -241,12 +241,24 @@ public class CoreDisputesService {
|
||||||
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
|
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
|
||||||
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
|
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
|
||||||
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
|
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||||
|
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
|
||||||
|
log.warn("Payout amount for buyer is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
|
||||||
|
HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()),
|
||||||
|
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
|
||||||
|
disputeResult.setBuyerPayoutAmountBeforeCost(trade.getWallet().getBalance());
|
||||||
|
}
|
||||||
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
|
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
|
||||||
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
|
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
|
||||||
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
|
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
|
||||||
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
|
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
|
||||||
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
|
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||||
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
|
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
|
||||||
|
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
|
||||||
|
log.warn("Payout amount for seller is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
|
||||||
|
HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()),
|
||||||
|
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
|
||||||
|
disputeResult.setSellerPayoutAmountBeforeCost(trade.getWallet().getBalance());
|
||||||
|
}
|
||||||
} else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
|
} else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
|
||||||
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
|
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
|
||||||
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
|
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
|
||||||
|
|
|
@ -149,7 +149,7 @@ public class CoreOffersService {
|
||||||
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
|
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
|
||||||
return getMyOffers().stream()
|
return getMyOffers().stream()
|
||||||
.filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
|
.filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
|
||||||
.sorted(openOfferPriceComparator(direction, CurrencyUtil.isTraditionalCurrency(currencyCode)))
|
.sorted(openOfferPriceComparator(direction))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ public class CoreOffersService {
|
||||||
String sourceOfferId,
|
String sourceOfferId,
|
||||||
Consumer<Transaction> resultHandler,
|
Consumer<Transaction> resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
|
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode());
|
||||||
openOfferManager.placeOffer(offer,
|
openOfferManager.placeOffer(offer,
|
||||||
useSavingsWallet,
|
useSavingsWallet,
|
||||||
triggerPriceAsLong,
|
triggerPriceAsLong,
|
||||||
|
@ -353,8 +353,7 @@ public class CoreOffersService {
|
||||||
if ("".equals(direction)) direction = null;
|
if ("".equals(direction)) direction = null;
|
||||||
if ("".equals(currencyCode)) currencyCode = null;
|
if ("".equals(currencyCode)) currencyCode = null;
|
||||||
var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction);
|
var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction);
|
||||||
var counterAssetCode = CurrencyUtil.isCryptoCurrency(currencyCode) ? offer.getOfferPayload().getBaseCurrencyCode() : offer.getOfferPayload().getCounterCurrencyCode();
|
var offerInWantedCurrency = currencyCode == null || offer.getCounterCurrencyCode().equalsIgnoreCase(currencyCode);
|
||||||
var offerInWantedCurrency = currencyCode == null || counterAssetCode.equalsIgnoreCase(currencyCode);
|
|
||||||
return offerOfWantedDirection && offerInWantedCurrency;
|
return offerOfWantedDirection && offerInWantedCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,17 +365,12 @@ public class CoreOffersService {
|
||||||
: priceComparator.get();
|
: priceComparator.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comparator<OpenOffer> openOfferPriceComparator(String direction, boolean isTraditional) {
|
private Comparator<OpenOffer> openOfferPriceComparator(String direction) {
|
||||||
// A buyer probably wants to see sell orders in price ascending order.
|
// A buyer probably wants to see sell orders in price ascending order.
|
||||||
// A seller probably wants to see buy orders in price descending order.
|
// A seller probably wants to see buy orders in price descending order.
|
||||||
if (isTraditional)
|
|
||||||
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
|
return direction.equalsIgnoreCase(OfferDirection.BUY.name())
|
||||||
? openOfferPriceComparator.get().reversed()
|
? openOfferPriceComparator.get().reversed()
|
||||||
: openOfferPriceComparator.get();
|
: openOfferPriceComparator.get();
|
||||||
else
|
|
||||||
return direction.equalsIgnoreCase(OfferDirection.SELL.name())
|
|
||||||
? openOfferPriceComparator.get().reversed()
|
|
||||||
: openOfferPriceComparator.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long priceStringToLong(String priceAsString, String currencyCode) {
|
private long priceStringToLong(String priceAsString, String currencyCode) {
|
||||||
|
|
|
@ -36,6 +36,8 @@ import haveno.core.payment.InstantCryptoCurrencyAccount;
|
||||||
import haveno.core.payment.PaymentAccount;
|
import haveno.core.payment.PaymentAccount;
|
||||||
import haveno.core.payment.PaymentAccountFactory;
|
import haveno.core.payment.PaymentAccountFactory;
|
||||||
import haveno.core.payment.payload.PaymentMethod;
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
|
import haveno.core.payment.validation.InteracETransferValidator;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
|
@ -48,19 +50,24 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class CorePaymentAccountsService {
|
public class CorePaymentAccountsService {
|
||||||
|
|
||||||
private final CoreAccountService accountService;
|
private final CoreAccountService accountService;
|
||||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||||
private final User user;
|
private final User user;
|
||||||
|
public final InteracETransferValidator interacETransferValidator;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CorePaymentAccountsService(CoreAccountService accountService,
|
public CorePaymentAccountsService(CoreAccountService accountService,
|
||||||
AccountAgeWitnessService accountAgeWitnessService,
|
AccountAgeWitnessService accountAgeWitnessService,
|
||||||
User user) {
|
User user,
|
||||||
|
InteracETransferValidator interacETransferValidator) {
|
||||||
this.accountService = accountService;
|
this.accountService = accountService;
|
||||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.interacETransferValidator = interacETransferValidator;
|
||||||
|
|
||||||
|
HavenoUtils.corePaymentAccountService = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
|
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
|
||||||
|
|
|
@ -74,9 +74,11 @@ class CorePriceService {
|
||||||
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
|
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
|
||||||
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
|
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
|
||||||
if (marketPrice == null) {
|
if (marketPrice == null) {
|
||||||
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
|
throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined?
|
||||||
|
} else if (!marketPrice.isExternallyProvidedPrice()) {
|
||||||
|
throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean
|
||||||
}
|
}
|
||||||
return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
|
return marketPrice.getPrice();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,8 +87,7 @@ class CorePriceService {
|
||||||
public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
|
public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
|
||||||
return priceFeedService.requestAllPrices().values().stream()
|
return priceFeedService.requestAllPrices().values().stream()
|
||||||
.map(marketPrice -> {
|
.map(marketPrice -> {
|
||||||
double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
|
return new MarketPriceInfo(marketPrice.getCurrencyCode(), marketPrice.getPrice());
|
||||||
return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice);
|
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
@ -100,12 +101,13 @@ class CorePriceService {
|
||||||
// Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used.
|
// Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used.
|
||||||
Comparator<Offer> offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder()));
|
Comparator<Offer> offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder()));
|
||||||
|
|
||||||
|
// TODO: remove this!!!
|
||||||
// Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is
|
// Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is
|
||||||
// considered as buying/selling crypto. Because of this, when viewing a xmr-crypto pair,
|
// considered as buying/selling crypto. Because of this, when viewing a xmr-crypto pair,
|
||||||
// the buy column is actually the sell column and vice versa. To maintain the expected
|
// the buy column is actually the sell column and vice versa. To maintain the expected
|
||||||
// ordering, we have to reverse the price comparator.
|
// ordering, we have to reverse the price comparator.
|
||||||
boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode);
|
//boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode);
|
||||||
if (isCrypto) offerPriceComparator = offerPriceComparator.reversed();
|
//if (isCrypto) offerPriceComparator = offerPriceComparator.reversed();
|
||||||
|
|
||||||
// Offer amounts are used for the secondary sort. They are sorted from high to low.
|
// Offer amounts are used for the secondary sort. They are sorted from high to low.
|
||||||
Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed();
|
Comparator<Offer> offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed();
|
||||||
|
@ -128,11 +130,11 @@ class CorePriceService {
|
||||||
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
|
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
|
||||||
accumulatedAmount += amount;
|
accumulatedAmount += amount;
|
||||||
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
|
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
|
||||||
buyTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount);
|
buyTM.put(priceAsDouble, accumulatedAmount);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create buyer hashmap {key:price, value:count}, uses TreeMap to sort by key (asc)
|
// Create seller hashmap {key:price, value:count}, uses TreeMap to sort by key (asc)
|
||||||
accumulatedAmount = 0;
|
accumulatedAmount = 0;
|
||||||
LinkedHashMap<Double,Double> sellTM = new LinkedHashMap<Double,Double>();
|
LinkedHashMap<Double,Double> sellTM = new LinkedHashMap<Double,Double>();
|
||||||
for(Offer offer: sellOffers){
|
for(Offer offer: sellOffers){
|
||||||
|
@ -141,7 +143,7 @@ class CorePriceService {
|
||||||
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
|
double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT);
|
||||||
accumulatedAmount += amount;
|
accumulatedAmount += amount;
|
||||||
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
|
double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent());
|
||||||
sellTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount);
|
sellTM.put(priceAsDouble, accumulatedAmount);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -155,20 +157,5 @@ class CorePriceService {
|
||||||
|
|
||||||
return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth);
|
return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* PriceProvider returns different values for crypto and traditional,
|
|
||||||
* e.g. 1 XMR = X USD
|
|
||||||
* but 1 DOGE = X XMR
|
|
||||||
* Here we convert all to:
|
|
||||||
* 1 XMR = X (FIAT or CRYPTO)
|
|
||||||
*/
|
|
||||||
private double mapPriceFeedServicePrice(double price, String currencyCode) {
|
|
||||||
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
|
|
||||||
return price;
|
|
||||||
}
|
|
||||||
return price == 0 ? 0 : 1 / price;
|
|
||||||
// TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,17 +123,14 @@ class CoreTradesService {
|
||||||
BigInteger amount = amountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(amountAsLong);
|
BigInteger amount = amountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(amountAsLong);
|
||||||
|
|
||||||
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
|
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
|
||||||
String currencyCode = offer.getCurrencyCode();
|
String currencyCode = offer.getCounterCurrencyCode();
|
||||||
OfferDirection direction = offer.getOfferPayload().getDirection();
|
OfferDirection direction = offer.getOfferPayload().getDirection();
|
||||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
|
BigInteger maxAmount = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
|
||||||
if (offer.getPrice() != null) {
|
if (offer.getPrice() != null) {
|
||||||
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
|
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
|
||||||
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
|
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount);
|
||||||
} else if (offer.isTraditionalOffer()
|
} else if (offer.isTraditionalOffer() && offer.isRange()) {
|
||||||
&& !amount.equals(offer.getMinAmount()) && !amount.equals(amount)) {
|
amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId());
|
||||||
// We only apply the rounding if the amount is variable (minAmount is lower as amount).
|
|
||||||
// Otherwise we could get an amount lower then the minAmount set by rounding
|
|
||||||
amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), maxTradeLimit, offer.getCurrencyCode(), offer.getPaymentMethodId());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +189,6 @@ class CoreTradesService {
|
||||||
verifyTradeIsNotClosed(tradeId);
|
verifyTradeIsNotClosed(tradeId);
|
||||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||||
log.info("Keeping funds received from trade {}", tradeId);
|
|
||||||
tradeManager.onTradeCompleted(trade);
|
tradeManager.onTradeCompleted(trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,12 +173,24 @@ class CoreWalletsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String relayXmrTx(String metadata) {
|
List<MoneroTxWallet> createXmrSweepTxs(String address) {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
verifyWalletsAreAvailable();
|
verifyWalletsAreAvailable();
|
||||||
verifyEncryptedWalletIsUnlocked();
|
verifyEncryptedWalletIsUnlocked();
|
||||||
try {
|
try {
|
||||||
return xmrWalletService.relayTx(metadata);
|
return xmrWalletService.createSweepTxs(address);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("", ex);
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> relayXmrTxs(List<String> metadatas) {
|
||||||
|
accountService.checkAccountOpen();
|
||||||
|
verifyWalletsAreAvailable();
|
||||||
|
verifyEncryptedWalletIsUnlocked();
|
||||||
|
try {
|
||||||
|
return xmrWalletService.relayTxs(metadatas);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("", ex);
|
log.error("", ex);
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import haveno.common.UserThread;
|
||||||
import haveno.common.app.DevEnv;
|
import haveno.common.app.DevEnv;
|
||||||
import haveno.common.config.BaseCurrencyNetwork;
|
import haveno.common.config.BaseCurrencyNetwork;
|
||||||
import haveno.common.config.Config;
|
import haveno.common.config.Config;
|
||||||
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.xmr.model.EncryptedConnectionList;
|
import haveno.core.xmr.model.EncryptedConnectionList;
|
||||||
|
@ -74,10 +75,13 @@ public final class XmrConnectionService {
|
||||||
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
||||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
||||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
||||||
|
private static final int MAX_CONSECUTIVE_ERRORS = 3; // max errors before switching connections
|
||||||
|
private static int numConsecutiveErrors = 0;
|
||||||
|
|
||||||
public enum XmrConnectionError {
|
public enum XmrConnectionFallbackType {
|
||||||
LOCAL,
|
LOCAL,
|
||||||
CUSTOM
|
CUSTOM,
|
||||||
|
PROVIDED
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Object lock = new Object();
|
private final Object lock = new Object();
|
||||||
|
@ -92,12 +96,12 @@ public final class XmrConnectionService {
|
||||||
private final MoneroConnectionManager connectionManager;
|
private final MoneroConnectionManager connectionManager;
|
||||||
private final EncryptedConnectionList connectionList;
|
private final EncryptedConnectionList connectionList;
|
||||||
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
|
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
|
||||||
private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
|
private final IntegerProperty numConnections = new SimpleIntegerProperty(-1);
|
||||||
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
|
||||||
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
||||||
private final DownloadListener downloadListener = new DownloadListener();
|
private final DownloadListener downloadListener = new DownloadListener();
|
||||||
@Getter
|
@Getter
|
||||||
private final ObjectProperty<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>();
|
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
|
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
|
||||||
private final LongProperty numUpdates = new SimpleLongProperty(0);
|
private final LongProperty numUpdates = new SimpleLongProperty(0);
|
||||||
|
@ -105,15 +109,15 @@ public final class XmrConnectionService {
|
||||||
|
|
||||||
private boolean isInitialized;
|
private boolean isInitialized;
|
||||||
private boolean pollInProgress;
|
private boolean pollInProgress;
|
||||||
private MoneroDaemonRpc daemon;
|
private MoneroDaemonRpc monerod;
|
||||||
private Boolean isConnected = false;
|
private Boolean isConnected = false;
|
||||||
@Getter
|
@Getter
|
||||||
private MoneroDaemonInfo lastInfo;
|
private MoneroDaemonInfo lastInfo;
|
||||||
private Long lastFallbackInvocation;
|
private Long lastFallbackInvocation;
|
||||||
private Long lastLogPollErrorTimestamp;
|
private Long lastLogPollErrorTimestamp;
|
||||||
private long lastLogDaemonNotSyncedTimestamp;
|
private long lastLogMonerodNotSyncedTimestamp;
|
||||||
private Long syncStartHeight;
|
private Long syncStartHeight;
|
||||||
private TaskLooper daemonPollLooper;
|
private TaskLooper monerodPollLooper;
|
||||||
private long lastRefreshPeriodMs;
|
private long lastRefreshPeriodMs;
|
||||||
@Getter
|
@Getter
|
||||||
private boolean isShutDownStarted;
|
private boolean isShutDownStarted;
|
||||||
|
@ -129,6 +133,7 @@ public final class XmrConnectionService {
|
||||||
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
|
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
|
||||||
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
|
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
|
||||||
private boolean fallbackApplied;
|
private boolean fallbackApplied;
|
||||||
|
private boolean usedSyncingLocalNodeBeforeStartup;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public XmrConnectionService(P2PService p2PService,
|
public XmrConnectionService(P2PService p2PService,
|
||||||
|
@ -156,7 +161,13 @@ public final class XmrConnectionService {
|
||||||
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
ThreadUtils.submitToPool(() -> initialize());
|
ThreadUtils.submitToPool(() -> {
|
||||||
|
try {
|
||||||
|
initialize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onHiddenServicePublished() {}
|
public void onHiddenServicePublished() {}
|
||||||
|
@ -180,16 +191,16 @@ public final class XmrConnectionService {
|
||||||
log.info("Shutting down {}", getClass().getSimpleName());
|
log.info("Shutting down {}", getClass().getSimpleName());
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
if (monerodPollLooper != null) monerodPollLooper.stop();
|
||||||
daemon = null;
|
monerod = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------ CONNECTION MANAGEMENT -------------------------
|
// ------------------------ CONNECTION MANAGEMENT -------------------------
|
||||||
|
|
||||||
public MoneroDaemonRpc getDaemon() {
|
public MoneroDaemonRpc getMonerod() {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
return this.daemon;
|
return this.monerod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getProxyUri() {
|
public String getProxyUri() {
|
||||||
|
@ -270,7 +281,7 @@ public final class XmrConnectionService {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
|
|
||||||
// user needs to authorize fallback on startup after using locally synced node
|
// user needs to authorize fallback on startup after using locally synced node
|
||||||
if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) {
|
if (fallbackRequiredBeforeConnectionSwitch()) {
|
||||||
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
|
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -283,6 +294,10 @@ public final class XmrConnectionService {
|
||||||
return bestConnection;
|
return bestConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean fallbackRequiredBeforeConnectionSwitch() {
|
||||||
|
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
|
||||||
|
}
|
||||||
|
|
||||||
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||||
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
|
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
|
||||||
}
|
}
|
||||||
|
@ -390,7 +405,7 @@ public final class XmrConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyConnection() {
|
public void verifyConnection() {
|
||||||
if (daemon == null) throw new RuntimeException("No connection to Monero node");
|
if (monerod == null) throw new RuntimeException("No connection to Monero node");
|
||||||
if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node");
|
if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node");
|
||||||
if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced");
|
if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced");
|
||||||
}
|
}
|
||||||
|
@ -433,6 +448,7 @@ public final class XmrConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSufficientPeersForBroadcast() {
|
public boolean hasSufficientPeersForBroadcast() {
|
||||||
|
if (numConnections.get() < 0) return true; // we don't know how many connections we have, but that's expected with restricted node
|
||||||
return numConnections.get() >= getMinBroadcastConnections();
|
return numConnections.get() >= getMinBroadcastConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,15 +474,20 @@ public final class XmrConnectionService {
|
||||||
|
|
||||||
public void fallbackToBestConnection() {
|
public void fallbackToBestConnection() {
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
fallbackApplied = true;
|
||||||
|
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||||
log.warn("Falling back to public nodes");
|
log.warn("Falling back to public nodes");
|
||||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
||||||
|
initializeConnections();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Falling back to provided nodes");
|
log.warn("Falling back to provided nodes");
|
||||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
|
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
|
||||||
}
|
|
||||||
fallbackApplied = true;
|
|
||||||
initializeConnections();
|
initializeConnections();
|
||||||
|
if (getConnection() == null) {
|
||||||
|
log.warn("No provided nodes available, falling back to public nodes");
|
||||||
|
fallbackToBestConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------- HELPERS --------------------------------
|
// ------------------------------- HELPERS --------------------------------
|
||||||
|
@ -548,8 +569,8 @@ public final class XmrConnectionService {
|
||||||
// register local node listener
|
// register local node listener
|
||||||
xmrLocalNode.addListener(new XmrLocalNodeListener() {
|
xmrLocalNode.addListener(new XmrLocalNodeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onNodeStarted(MoneroDaemonRpc daemon) {
|
public void onNodeStarted(MoneroDaemonRpc monerod) {
|
||||||
log.info("Local monero node started, height={}", daemon.getHeight());
|
log.info("Local monero node started, height={}", monerod.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -578,8 +599,8 @@ public final class XmrConnectionService {
|
||||||
setConnection(connection.getUri());
|
setConnection(connection.getUri());
|
||||||
|
|
||||||
// reset error connecting to local node
|
// reset error connecting to local node
|
||||||
if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) {
|
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
|
||||||
connectionServiceError.set(null);
|
connectionServiceFallbackType.set(null);
|
||||||
}
|
}
|
||||||
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
||||||
MoneroRpcConnection bestConnection = getBestConnection();
|
MoneroRpcConnection bestConnection = getBestConnection();
|
||||||
|
@ -602,9 +623,11 @@ public final class XmrConnectionService {
|
||||||
// add default connections
|
// add default connections
|
||||||
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
|
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
|
||||||
if (node.hasClearNetAddress()) {
|
if (node.hasClearNetAddress()) {
|
||||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
|
||||||
|
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||||
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (node.hasOnionAddress()) {
|
if (node.hasOnionAddress()) {
|
||||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||||
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
||||||
|
@ -615,9 +638,11 @@ public final class XmrConnectionService {
|
||||||
// add default connections
|
// add default connections
|
||||||
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||||
if (node.hasClearNetAddress()) {
|
if (node.hasClearNetAddress()) {
|
||||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
|
||||||
|
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||||
addConnection(connection);
|
addConnection(connection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (node.hasOnionAddress()) {
|
if (node.hasOnionAddress()) {
|
||||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||||
addConnection(connection);
|
addConnection(connection);
|
||||||
|
@ -632,6 +657,11 @@ public final class XmrConnectionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set if last node was locally syncing
|
||||||
|
if (!isInitialized) {
|
||||||
|
usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain();
|
||||||
|
}
|
||||||
|
|
||||||
// set connection proxies
|
// set connection proxies
|
||||||
log.info("TOR proxy URI: " + getProxyUri());
|
log.info("TOR proxy URI: " + getProxyUri());
|
||||||
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
||||||
|
@ -666,43 +696,30 @@ public final class XmrConnectionService {
|
||||||
onConnectionChanged(connectionManager.getConnection());
|
onConnectionChanged(connectionManager.getConnection());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean lastUsedLocalSyncingNode() {
|
public void startLocalNode() throws Exception {
|
||||||
return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startLocalNode() {
|
|
||||||
|
|
||||||
// cannot start local node as seed node
|
// cannot start local node as seed node
|
||||||
if (HavenoUtils.isSeedNode()) {
|
if (HavenoUtils.isSeedNode()) {
|
||||||
throw new RuntimeException("Cannot start local node on seed node");
|
throw new RuntimeException("Cannot start local node on seed node");
|
||||||
}
|
}
|
||||||
|
|
||||||
// start local node if offline and used as last connection
|
// start local node
|
||||||
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
|
|
||||||
try {
|
|
||||||
log.info("Starting local node");
|
log.info("Starting local node");
|
||||||
xmrLocalNode.start();
|
xmrLocalNode.start();
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Unable to start local monero node, error={}\n", e.getMessage(), e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Local node is not offline and used as last connection");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||||
if (isShutDownStarted || !accountService.isAccountOpen()) return;
|
if (isShutDownStarted || !accountService.isAccountOpen()) return;
|
||||||
if (currentConnection == null) {
|
if (currentConnection == null) {
|
||||||
log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
|
log.warn("Setting monerod connection to null", new Throwable("Stack trace"));
|
||||||
}
|
}
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (currentConnection == null) {
|
if (currentConnection == null) {
|
||||||
daemon = null;
|
monerod = null;
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
connectionList.setCurrentConnectionUri(null);
|
connectionList.setCurrentConnectionUri(null);
|
||||||
} else {
|
} else {
|
||||||
daemon = new MoneroDaemonRpc(currentConnection);
|
monerod = new MoneroDaemonRpc(currentConnection);
|
||||||
isConnected = currentConnection.isConnected();
|
isConnected = currentConnection.isConnected();
|
||||||
connectionList.removeConnection(currentConnection.getUri());
|
connectionList.removeConnection(currentConnection.getUri());
|
||||||
connectionList.addConnection(currentConnection);
|
connectionList.addConnection(currentConnection);
|
||||||
|
@ -717,11 +734,11 @@ public final class XmrConnectionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update key image poller
|
// update key image poller
|
||||||
keyImagePoller.setDaemon(getDaemon());
|
keyImagePoller.setMonerod(getMonerod());
|
||||||
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
||||||
|
|
||||||
// update polling
|
// update polling
|
||||||
doPollDaemon();
|
doPollMonerod();
|
||||||
if (currentConnection != getConnection()) return; // polling can change connection
|
if (currentConnection != getConnection()) return; // polling can change connection
|
||||||
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
|
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
|
||||||
|
|
||||||
|
@ -741,74 +758,87 @@ public final class XmrConnectionService {
|
||||||
|
|
||||||
private void startPolling() {
|
private void startPolling() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
if (monerodPollLooper != null) monerodPollLooper.stop();
|
||||||
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
monerodPollLooper = new TaskLooper(() -> pollMonerod());
|
||||||
daemonPollLooper.start(getInternalRefreshPeriodMs());
|
monerodPollLooper.start(getInternalRefreshPeriodMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopPolling() {
|
private void stopPolling() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (daemonPollLooper != null) {
|
if (monerodPollLooper != null) {
|
||||||
daemonPollLooper.stop();
|
monerodPollLooper.stop();
|
||||||
daemonPollLooper = null;
|
monerodPollLooper = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pollDaemon() {
|
private void pollMonerod() {
|
||||||
if (pollInProgress) return;
|
if (pollInProgress) return;
|
||||||
doPollDaemon();
|
doPollMonerod();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doPollDaemon() {
|
private void doPollMonerod() {
|
||||||
synchronized (pollLock) {
|
synchronized (pollLock) {
|
||||||
pollInProgress = true;
|
pollInProgress = true;
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// poll daemon
|
// poll monerod
|
||||||
if (daemon == null) switchToBestConnection();
|
if (monerod == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
|
||||||
try {
|
try {
|
||||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
if (monerod == null) throw new RuntimeException("No connection to Monero daemon");
|
||||||
lastInfo = daemon.getInfo();
|
lastInfo = monerod.getInfo();
|
||||||
|
numConsecutiveErrors = 0;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
// skip handling if shutting down
|
// skip handling if shutting down
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
|
|
||||||
// invoke fallback handling on startup error
|
// skip error handling up to max attempts
|
||||||
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode();
|
numConsecutiveErrors++;
|
||||||
if (lastInfo == null && canFallback) {
|
if (numConsecutiveErrors <= MAX_CONSECUTIVE_ERRORS) {
|
||||||
if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
|
return;
|
||||||
lastFallbackInvocation = System.currentTimeMillis();
|
|
||||||
if (lastUsedLocalSyncingNode()) {
|
|
||||||
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
|
|
||||||
connectionServiceError.set(XmrConnectionError.LOCAL);
|
|
||||||
} else {
|
} else {
|
||||||
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
|
numConsecutiveErrors = 0; // reset error count
|
||||||
connectionServiceError.set(XmrConnectionError.CUSTOM);
|
}
|
||||||
|
|
||||||
|
// invoke fallback handling on startup error
|
||||||
|
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
|
||||||
|
if (lastInfo == null && canFallback) {
|
||||||
|
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
|
||||||
|
lastFallbackInvocation = System.currentTimeMillis();
|
||||||
|
if (usedSyncingLocalNodeBeforeStartup) {
|
||||||
|
log.warn("Failed to fetch monerod info from local connection on startup: " + e.getMessage());
|
||||||
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
|
||||||
|
} else if (isProvidedConnections()) {
|
||||||
|
log.warn("Failed to fetch monerod info from provided connections on startup: " + e.getMessage());
|
||||||
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to fetch monerod info from custom connection on startup: " + e.getMessage());
|
||||||
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log error message periodically
|
// log error message periodically
|
||||||
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
|
if (lastWarningOutsidePeriod()) {
|
||||||
log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage());
|
MoneroRpcConnection connection = getConnection();
|
||||||
|
log.warn("Error fetching daemon info after max attempts. Trying to switch to best connection. monerod={}, error={}", connection == null ? "null" : connection.getUri(), e.getMessage());
|
||||||
if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e));
|
if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e));
|
||||||
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
// switch to best connection
|
// switch to best connection
|
||||||
switchToBestConnection();
|
switchToBestConnection();
|
||||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
|
if (monerod == null) throw new RuntimeException("No connection to Monero daemon after error handling");
|
||||||
lastInfo = daemon.getInfo(); // caught internally if still fails
|
lastInfo = monerod.getInfo(); // caught internally if still fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// connected to daemon
|
// connected to monerod
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
connectionServiceError.set(null);
|
connectionServiceFallbackType.set(null);
|
||||||
|
|
||||||
// determine if blockchain is syncing locally
|
// determine if blockchain is syncing locally
|
||||||
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
|
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
|
||||||
|
@ -816,10 +846,10 @@ public final class XmrConnectionService {
|
||||||
// write sync status to preferences
|
// write sync status to preferences
|
||||||
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
|
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
|
||||||
|
|
||||||
// throttle warnings if daemon not synced
|
// throttle warnings if monerod not synced
|
||||||
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) {
|
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogMonerodNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) {
|
||||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight());
|
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight());
|
||||||
lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis();
|
lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
// announce connection change if refresh period changes
|
// announce connection change if refresh period changes
|
||||||
|
@ -829,6 +859,9 @@ public final class XmrConnectionService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the number of connections, which is only available if not restricted
|
||||||
|
int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections();
|
||||||
|
|
||||||
// update properties on user thread
|
// update properties on user thread
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
|
|
||||||
|
@ -854,15 +887,22 @@ public final class XmrConnectionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
connections.set(availableConnections);
|
connections.set(availableConnections);
|
||||||
numConnections.set(availableConnections.size());
|
numConnections.set(numOutgoingConnections);
|
||||||
|
|
||||||
// notify update
|
// notify update
|
||||||
numUpdates.set(numUpdates.get() + 1);
|
numUpdates.set(numUpdates.get() + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// invoke error handling if no connections
|
||||||
|
if (numOutgoingConnections == 0) {
|
||||||
|
String errorMsg = "The Monero node has no connected peers. It may be experiencing a network connectivity issue.";
|
||||||
|
log.warn(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
// handle error recovery
|
// handle error recovery
|
||||||
if (lastLogPollErrorTimestamp != null) {
|
if (lastLogPollErrorTimestamp != null) {
|
||||||
log.info("Successfully fetched daemon info after previous error");
|
log.info("Successfully fetched monerod info after previous error");
|
||||||
lastLogPollErrorTimestamp = null;
|
lastLogPollErrorTimestamp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -870,25 +910,40 @@ public final class XmrConnectionService {
|
||||||
getConnectionServiceErrorMsg().set(null);
|
getConnectionServiceErrorMsg().set(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
// not connected to daemon
|
// not connected to monerod
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
|
|
||||||
// skip if shut down
|
// skip if shut down
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
|
|
||||||
|
// format error message
|
||||||
|
String errorMsg = e.getMessage();
|
||||||
|
if (errorMsg != null && errorMsg.contains(": ")) {
|
||||||
|
errorMsg = errorMsg.substring(errorMsg.indexOf(": ") + 2); // strip exception class
|
||||||
|
}
|
||||||
|
errorMsg = Res.get("popup.warning.moneroConnection", errorMsg);
|
||||||
|
|
||||||
// set error message
|
// set error message
|
||||||
getConnectionServiceErrorMsg().set(e.getMessage());
|
getConnectionServiceErrorMsg().set(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
pollInProgress = false;
|
pollInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean lastWarningOutsidePeriod() {
|
||||||
|
return lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isFixedConnection() {
|
private boolean isFixedConnection() {
|
||||||
return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
|
return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCustomConnections() {
|
private boolean isCustomConnections() {
|
||||||
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isProvidedConnections() {
|
||||||
|
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,17 +109,18 @@ public class XmrLocalNode {
|
||||||
public boolean shouldBeIgnored() {
|
public boolean shouldBeIgnored() {
|
||||||
if (config.ignoreLocalXmrNode) return true;
|
if (config.ignoreLocalXmrNode) return true;
|
||||||
|
|
||||||
// determine if local node is configured
|
// ignore if fixed connection is not local
|
||||||
|
if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
|
||||||
|
|
||||||
|
// check if local node is within configuration
|
||||||
boolean hasConfiguredLocalNode = false;
|
boolean hasConfiguredLocalNode = false;
|
||||||
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||||
if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) {
|
if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
|
||||||
hasConfiguredLocalNode = true;
|
hasConfiguredLocalNode = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasConfiguredLocalNode) return true;
|
return !hasConfiguredLocalNode;
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(XmrLocalNodeListener listener) {
|
public void addListener(XmrLocalNodeListener listener) {
|
||||||
|
|
|
@ -129,7 +129,7 @@ public class OfferInfo implements Payload {
|
||||||
public static OfferInfo toMyOfferInfo(OpenOffer openOffer) {
|
public static OfferInfo toMyOfferInfo(OpenOffer openOffer) {
|
||||||
// An OpenOffer is always my offer.
|
// An OpenOffer is always my offer.
|
||||||
var offer = openOffer.getOffer();
|
var offer = openOffer.getOffer();
|
||||||
var currencyCode = offer.getCurrencyCode();
|
var currencyCode = offer.getCounterCurrencyCode();
|
||||||
var isActivated = !openOffer.isDeactivated();
|
var isActivated = !openOffer.isDeactivated();
|
||||||
Optional<Price> optionalTriggerPrice = openOffer.getTriggerPrice() > 0
|
Optional<Price> optionalTriggerPrice = openOffer.getTriggerPrice() > 0
|
||||||
? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice()))
|
? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice()))
|
||||||
|
@ -150,7 +150,7 @@ public class OfferInfo implements Payload {
|
||||||
private static OfferInfoBuilder getBuilder(Offer offer) {
|
private static OfferInfoBuilder getBuilder(Offer offer) {
|
||||||
// OfferInfo protos are passed to API client, and some field
|
// OfferInfo protos are passed to API client, and some field
|
||||||
// values are converted to displayable, unambiguous form.
|
// values are converted to displayable, unambiguous form.
|
||||||
var currencyCode = offer.getCurrencyCode();
|
var currencyCode = offer.getCounterCurrencyCode();
|
||||||
var preciseOfferPrice = reformatMarketPrice(
|
var preciseOfferPrice = reformatMarketPrice(
|
||||||
requireNonNull(offer.getPrice()).toPlainString(),
|
requireNonNull(offer.getPrice()).toPlainString(),
|
||||||
currencyCode);
|
currencyCode);
|
||||||
|
|
|
@ -78,7 +78,15 @@ public final class PaymentAccountForm implements PersistablePayload {
|
||||||
CASH_APP,
|
CASH_APP,
|
||||||
PAYPAL,
|
PAYPAL,
|
||||||
VENMO,
|
VENMO,
|
||||||
PAYSAFE;
|
PAYSAFE,
|
||||||
|
WECHAT_PAY,
|
||||||
|
ALI_PAY,
|
||||||
|
SWISH,
|
||||||
|
TRANSFERWISE_USD,
|
||||||
|
AMAZON_GIFT_CARD,
|
||||||
|
ACH_TRANSFER,
|
||||||
|
INTERAC_E_TRANSFER,
|
||||||
|
US_POSTAL_MONEY_ORDER;
|
||||||
|
|
||||||
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
|
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
|
||||||
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());
|
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class TradeInfo implements Payload {
|
||||||
|
|
||||||
private static final Function<Trade, String> toPreciseTradePrice = (trade) ->
|
private static final Function<Trade, String> toPreciseTradePrice = (trade) ->
|
||||||
reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(),
|
reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(),
|
||||||
trade.getOffer().getCurrencyCode());
|
trade.getOffer().getCounterCurrencyCode());
|
||||||
|
|
||||||
// Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol).
|
// Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol).
|
||||||
private final OfferInfo offer;
|
private final OfferInfo offer;
|
||||||
|
@ -91,14 +91,19 @@ public class TradeInfo implements Payload {
|
||||||
private final boolean isDepositsPublished;
|
private final boolean isDepositsPublished;
|
||||||
private final boolean isDepositsConfirmed;
|
private final boolean isDepositsConfirmed;
|
||||||
private final boolean isDepositsUnlocked;
|
private final boolean isDepositsUnlocked;
|
||||||
|
private final boolean isDepositsFinalized;
|
||||||
private final boolean isPaymentSent;
|
private final boolean isPaymentSent;
|
||||||
private final boolean isPaymentReceived;
|
private final boolean isPaymentReceived;
|
||||||
private final boolean isPayoutPublished;
|
private final boolean isPayoutPublished;
|
||||||
private final boolean isPayoutConfirmed;
|
private final boolean isPayoutConfirmed;
|
||||||
private final boolean isPayoutUnlocked;
|
private final boolean isPayoutUnlocked;
|
||||||
|
private final boolean isPayoutFinalized;
|
||||||
private final boolean isCompleted;
|
private final boolean isCompleted;
|
||||||
private final String contractAsJson;
|
private final String contractAsJson;
|
||||||
private final ContractInfo contract;
|
private final ContractInfo contract;
|
||||||
|
private final long startTime;
|
||||||
|
private final long maxDurationMs;
|
||||||
|
private final long deadlineTime;
|
||||||
|
|
||||||
public TradeInfo(TradeInfoV1Builder builder) {
|
public TradeInfo(TradeInfoV1Builder builder) {
|
||||||
this.offer = builder.getOffer();
|
this.offer = builder.getOffer();
|
||||||
|
@ -132,14 +137,19 @@ public class TradeInfo implements Payload {
|
||||||
this.isDepositsPublished = builder.isDepositsPublished();
|
this.isDepositsPublished = builder.isDepositsPublished();
|
||||||
this.isDepositsConfirmed = builder.isDepositsConfirmed();
|
this.isDepositsConfirmed = builder.isDepositsConfirmed();
|
||||||
this.isDepositsUnlocked = builder.isDepositsUnlocked();
|
this.isDepositsUnlocked = builder.isDepositsUnlocked();
|
||||||
|
this.isDepositsFinalized = builder.isDepositsFinalized();
|
||||||
this.isPaymentSent = builder.isPaymentSent();
|
this.isPaymentSent = builder.isPaymentSent();
|
||||||
this.isPaymentReceived = builder.isPaymentReceived();
|
this.isPaymentReceived = builder.isPaymentReceived();
|
||||||
this.isPayoutPublished = builder.isPayoutPublished();
|
this.isPayoutPublished = builder.isPayoutPublished();
|
||||||
this.isPayoutConfirmed = builder.isPayoutConfirmed();
|
this.isPayoutConfirmed = builder.isPayoutConfirmed();
|
||||||
this.isPayoutUnlocked = builder.isPayoutUnlocked();
|
this.isPayoutUnlocked = builder.isPayoutUnlocked();
|
||||||
|
this.isPayoutFinalized = builder.isPayoutFinalized();
|
||||||
this.isCompleted = builder.isCompleted();
|
this.isCompleted = builder.isCompleted();
|
||||||
this.contractAsJson = builder.getContractAsJson();
|
this.contractAsJson = builder.getContractAsJson();
|
||||||
this.contract = builder.getContract();
|
this.contract = builder.getContract();
|
||||||
|
this.startTime = builder.getStartTime();
|
||||||
|
this.maxDurationMs = builder.getMaxDurationMs();
|
||||||
|
this.deadlineTime = builder.getDeadlineTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TradeInfo toTradeInfo(Trade trade) {
|
public static TradeInfo toTradeInfo(Trade trade) {
|
||||||
|
@ -193,15 +203,20 @@ public class TradeInfo implements Payload {
|
||||||
.withIsDepositsPublished(trade.isDepositsPublished())
|
.withIsDepositsPublished(trade.isDepositsPublished())
|
||||||
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
|
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
|
||||||
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
|
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
|
||||||
|
.withIsDepositsFinalized(trade.isDepositsFinalized())
|
||||||
.withIsPaymentSent(trade.isPaymentSent())
|
.withIsPaymentSent(trade.isPaymentSent())
|
||||||
.withIsPaymentReceived(trade.isPaymentReceived())
|
.withIsPaymentReceived(trade.isPaymentReceived())
|
||||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||||
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
|
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
|
||||||
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
|
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
|
||||||
|
.withIsPayoutFinalized(trade.isPayoutFinalized())
|
||||||
.withIsCompleted(trade.isCompleted())
|
.withIsCompleted(trade.isCompleted())
|
||||||
.withContractAsJson(trade.getContractAsJson())
|
.withContractAsJson(trade.getContractAsJson())
|
||||||
.withContract(contractInfo)
|
.withContract(contractInfo)
|
||||||
.withOffer(toOfferInfo(trade.getOffer()))
|
.withOffer(toOfferInfo(trade.getOffer()))
|
||||||
|
.withStartTime(trade.getStartDate().getTime())
|
||||||
|
.withMaxDurationMs(trade.getMaxTradePeriod())
|
||||||
|
.withDeadlineTime(trade.getMaxTradePeriodDate().getTime())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,14 +258,19 @@ public class TradeInfo implements Payload {
|
||||||
.setIsDepositsPublished(isDepositsPublished)
|
.setIsDepositsPublished(isDepositsPublished)
|
||||||
.setIsDepositsConfirmed(isDepositsConfirmed)
|
.setIsDepositsConfirmed(isDepositsConfirmed)
|
||||||
.setIsDepositsUnlocked(isDepositsUnlocked)
|
.setIsDepositsUnlocked(isDepositsUnlocked)
|
||||||
|
.setIsDepositsFinalized(isDepositsFinalized)
|
||||||
.setIsPaymentSent(isPaymentSent)
|
.setIsPaymentSent(isPaymentSent)
|
||||||
.setIsPaymentReceived(isPaymentReceived)
|
.setIsPaymentReceived(isPaymentReceived)
|
||||||
.setIsCompleted(isCompleted)
|
.setIsCompleted(isCompleted)
|
||||||
.setIsPayoutPublished(isPayoutPublished)
|
.setIsPayoutPublished(isPayoutPublished)
|
||||||
.setIsPayoutConfirmed(isPayoutConfirmed)
|
.setIsPayoutConfirmed(isPayoutConfirmed)
|
||||||
.setIsPayoutUnlocked(isPayoutUnlocked)
|
.setIsPayoutUnlocked(isPayoutUnlocked)
|
||||||
|
.setIsPayoutFinalized(isPayoutFinalized)
|
||||||
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
||||||
.setContract(contract.toProtoMessage())
|
.setContract(contract.toProtoMessage())
|
||||||
|
.setStartTime(startTime)
|
||||||
|
.setMaxDurationMs(maxDurationMs)
|
||||||
|
.setDeadlineTime(deadlineTime)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,14 +307,19 @@ public class TradeInfo implements Payload {
|
||||||
.withIsDepositsPublished(proto.getIsDepositsPublished())
|
.withIsDepositsPublished(proto.getIsDepositsPublished())
|
||||||
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
|
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
|
||||||
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
|
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
|
||||||
|
.withIsDepositsFinalized(proto.getIsDepositsFinalized())
|
||||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||||
.withIsCompleted(proto.getIsCompleted())
|
.withIsCompleted(proto.getIsCompleted())
|
||||||
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
||||||
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
|
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
|
||||||
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
|
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
|
||||||
|
.withIsPayoutFinalized(proto.getIsPayoutFinalized())
|
||||||
.withContractAsJson(proto.getContractAsJson())
|
.withContractAsJson(proto.getContractAsJson())
|
||||||
.withContract((ContractInfo.fromProto(proto.getContract())))
|
.withContract((ContractInfo.fromProto(proto.getContract())))
|
||||||
|
.withStartTime(proto.getStartTime())
|
||||||
|
.withMaxDurationMs(proto.getMaxDurationMs())
|
||||||
|
.withDeadlineTime(proto.getDeadlineTime())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,15 +355,20 @@ public class TradeInfo implements Payload {
|
||||||
", isDepositsPublished=" + isDepositsPublished + "\n" +
|
", isDepositsPublished=" + isDepositsPublished + "\n" +
|
||||||
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
|
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
|
||||||
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
|
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
|
||||||
|
", isDepositsFinalized=" + isDepositsFinalized + "\n" +
|
||||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||||
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
|
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
|
||||||
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
|
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
|
||||||
|
", isPayoutFinalized=" + isPayoutFinalized + "\n" +
|
||||||
", isCompleted=" + isCompleted + "\n" +
|
", isCompleted=" + isCompleted + "\n" +
|
||||||
", offer=" + offer + "\n" +
|
", offer=" + offer + "\n" +
|
||||||
", contractAsJson=" + contractAsJson + "\n" +
|
", contractAsJson=" + contractAsJson + "\n" +
|
||||||
", contract=" + contract + "\n" +
|
", contract=" + contract + "\n" +
|
||||||
|
", startTime=" + startTime + "\n" +
|
||||||
|
", maxDurationMs=" + maxDurationMs + "\n" +
|
||||||
|
", deadlineTime=" + deadlineTime + "\n" +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,15 +64,20 @@ public final class TradeInfoV1Builder {
|
||||||
private boolean isDepositsPublished;
|
private boolean isDepositsPublished;
|
||||||
private boolean isDepositsConfirmed;
|
private boolean isDepositsConfirmed;
|
||||||
private boolean isDepositsUnlocked;
|
private boolean isDepositsUnlocked;
|
||||||
|
private boolean isDepositsFinalized;
|
||||||
private boolean isPaymentSent;
|
private boolean isPaymentSent;
|
||||||
private boolean isPaymentReceived;
|
private boolean isPaymentReceived;
|
||||||
private boolean isPayoutPublished;
|
private boolean isPayoutPublished;
|
||||||
private boolean isPayoutConfirmed;
|
private boolean isPayoutConfirmed;
|
||||||
private boolean isPayoutUnlocked;
|
private boolean isPayoutUnlocked;
|
||||||
|
private boolean isPayoutFinalized;
|
||||||
private boolean isCompleted;
|
private boolean isCompleted;
|
||||||
private String contractAsJson;
|
private String contractAsJson;
|
||||||
private ContractInfo contract;
|
private ContractInfo contract;
|
||||||
private String closingStatus;
|
private String closingStatus;
|
||||||
|
private long startTime;
|
||||||
|
private long maxDurationMs;
|
||||||
|
private long deadlineTime;
|
||||||
|
|
||||||
public TradeInfoV1Builder withOffer(OfferInfo offer) {
|
public TradeInfoV1Builder withOffer(OfferInfo offer) {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
|
@ -239,6 +244,11 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) {
|
||||||
|
this.isDepositsFinalized = isDepositsFinalized;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) {
|
public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) {
|
||||||
this.isPaymentSent = isPaymentSent;
|
this.isPaymentSent = isPaymentSent;
|
||||||
return this;
|
return this;
|
||||||
|
@ -264,6 +274,11 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) {
|
||||||
|
this.isPayoutFinalized = isPayoutFinalized;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
|
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
|
||||||
this.isCompleted = isCompleted;
|
this.isCompleted = isCompleted;
|
||||||
return this;
|
return this;
|
||||||
|
@ -284,6 +299,21 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withStartTime(long startTime) {
|
||||||
|
this.startTime = startTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withMaxDurationMs(long maxDurationMs) {
|
||||||
|
this.maxDurationMs = maxDurationMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TradeInfoV1Builder withDeadlineTime(long deadlineTime) {
|
||||||
|
this.deadlineTime = deadlineTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TradeInfo build() {
|
public TradeInfo build() {
|
||||||
return new TradeInfo(this);
|
return new TradeInfo(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||||
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
|
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
|
||||||
acceptedHandler.run();
|
acceptedHandler.run();
|
||||||
});
|
});
|
||||||
havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show));
|
havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show));
|
||||||
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
|
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
|
||||||
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
|
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
|
||||||
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
|
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
|
||||||
|
|
|
@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
|
||||||
import haveno.core.alert.PrivateNotificationPayload;
|
import haveno.core.alert.PrivateNotificationPayload;
|
||||||
import haveno.core.api.CoreContext;
|
import haveno.core.api.CoreContext;
|
||||||
import haveno.core.api.XmrConnectionService;
|
import haveno.core.api.XmrConnectionService;
|
||||||
import haveno.core.api.XmrConnectionService.XmrConnectionError;
|
import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
|
||||||
import haveno.core.api.XmrLocalNode;
|
import haveno.core.api.XmrLocalNode;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.offer.OpenOfferManager;
|
import haveno.core.offer.OpenOfferManager;
|
||||||
|
@ -159,7 +159,7 @@ public class HavenoSetup {
|
||||||
rejectedTxErrorMessageHandler;
|
rejectedTxErrorMessageHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler;
|
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
||||||
|
@ -431,9 +431,9 @@ public class HavenoSetup {
|
||||||
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||||
|
|
||||||
// listen for fallback handling
|
// listen for fallback handling
|
||||||
getConnectionServiceError().addListener((observable, oldValue, newValue) -> {
|
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
|
||||||
if (displayMoneroConnectionErrorHandler == null) return;
|
if (displayMoneroConnectionFallbackHandler == null) return;
|
||||||
displayMoneroConnectionErrorHandler.accept(newValue);
|
displayMoneroConnectionFallbackHandler.accept(newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("Init P2P network");
|
log.info("Init P2P network");
|
||||||
|
@ -735,8 +735,8 @@ public class HavenoSetup {
|
||||||
return xmrConnectionService.getConnectionServiceErrorMsg();
|
return xmrConnectionService.getConnectionServiceErrorMsg();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<XmrConnectionError> getConnectionServiceError() {
|
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
|
||||||
return xmrConnectionService.getConnectionServiceError();
|
return xmrConnectionService.getConnectionServiceFallbackType();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty getTopErrorMsg() {
|
public StringProperty getTopErrorMsg() {
|
||||||
|
|
|
@ -94,7 +94,7 @@ public class P2PNetworkSetup {
|
||||||
if (warning != null && p2pPeers == 0) {
|
if (warning != null && p2pPeers == 0) {
|
||||||
result = warning;
|
result = warning;
|
||||||
} else {
|
} else {
|
||||||
String p2pInfo = ((int) numXmrPeers > 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers);
|
String p2pInfo = ((int) numXmrPeers >= 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers);
|
||||||
if (dataReceived && hiddenService) {
|
if (dataReceived && hiddenService) {
|
||||||
result = p2pInfo;
|
result = p2pInfo;
|
||||||
} else if (p2pPeers == 0)
|
} else if (p2pPeers == 0)
|
||||||
|
|
|
@ -188,6 +188,8 @@ public class WalletAppSetup {
|
||||||
} else {
|
} else {
|
||||||
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage()));
|
xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage()));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
xmrConnectionService.getConnectionServiceErrorMsg().set(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -151,6 +151,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
||||||
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// shut down trade and wallet services
|
// shut down trade and wallet services
|
||||||
log.info("Shutting down trade and wallet services");
|
log.info("Shutting down trade and wallet services");
|
||||||
|
@ -161,13 +162,12 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
||||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||||
injector.getInstance(WalletsSetup.class).shutDown();
|
injector.getInstance(WalletsSetup.class).shutDown();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// we wait max 5 sec.
|
// we wait max 5 sec.
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
log.info("Graceful shutdown caused a timeout. Exiting now.");
|
log.warn("Graceful shutdown caused a timeout. Exiting now.");
|
||||||
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
||||||
});
|
});
|
||||||
}, 5);
|
}, 5);
|
||||||
|
|
|
@ -84,9 +84,9 @@ public class FilterManager {
|
||||||
private final ConfigFileEditor configFileEditor;
|
private final ConfigFileEditor configFileEditor;
|
||||||
private final ProvidersRepository providersRepository;
|
private final ProvidersRepository providersRepository;
|
||||||
private final boolean ignoreDevMsg;
|
private final boolean ignoreDevMsg;
|
||||||
|
private final boolean useDevPrivilegeKeys;
|
||||||
private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<Filter> filterProperty = new SimpleObjectProperty<>();
|
||||||
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
|
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||||
private final List<String> publicKeys;
|
|
||||||
private ECKey filterSigningKey;
|
private ECKey filterSigningKey;
|
||||||
private final Set<Filter> invalidFilters = new HashSet<>();
|
private final Set<Filter> invalidFilters = new HashSet<>();
|
||||||
private Consumer<String> filterWarningHandler;
|
private Consumer<String> filterWarningHandler;
|
||||||
|
@ -113,16 +113,31 @@ public class FilterManager {
|
||||||
this.configFileEditor = new ConfigFileEditor(config.configFile);
|
this.configFileEditor = new ConfigFileEditor(config.configFile);
|
||||||
this.providersRepository = providersRepository;
|
this.providersRepository = providersRepository;
|
||||||
this.ignoreDevMsg = ignoreDevMsg;
|
this.ignoreDevMsg = ignoreDevMsg;
|
||||||
|
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
|
||||||
publicKeys = useDevPrivilegeKeys ?
|
|
||||||
Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) :
|
|
||||||
List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27",
|
|
||||||
"029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f",
|
|
||||||
"034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9");
|
|
||||||
|
|
||||||
banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork);
|
banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected List<String> getPubKeyList() {
|
||||||
|
switch (Config.baseCurrencyNetwork()) {
|
||||||
|
case XMR_LOCAL:
|
||||||
|
if (useDevPrivilegeKeys) return Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY);
|
||||||
|
return List.of(
|
||||||
|
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
|
||||||
|
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492",
|
||||||
|
"026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519");
|
||||||
|
case XMR_STAGENET:
|
||||||
|
return List.of(
|
||||||
|
"03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
|
||||||
|
"02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
|
||||||
|
"0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
|
||||||
|
case XMR_MAINNET:
|
||||||
|
return List.of();
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// API
|
// API
|
||||||
|
@ -587,16 +602,16 @@ public class FilterManager {
|
||||||
"but the new version does not recognize it as valid filter): " +
|
"but the new version does not recognize it as valid filter): " +
|
||||||
"signerPubKeyAsHex from filter is not part of our pub key list. " +
|
"signerPubKeyAsHex from filter is not part of our pub key list. " +
|
||||||
"signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}",
|
"signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}",
|
||||||
signerPubKeyAsHex, publicKeys, new Date(filter.getCreationDate()));
|
signerPubKeyAsHex, getPubKeyList(), new Date(filter.getCreationDate()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPublicKeyInList(String pubKeyAsHex) {
|
private boolean isPublicKeyInList(String pubKeyAsHex) {
|
||||||
boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex);
|
boolean isPublicKeyInList = getPubKeyList().contains(pubKeyAsHex);
|
||||||
if (!isPublicKeyInList) {
|
if (!isPublicKeyInList) {
|
||||||
log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys);
|
log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, getPubKeyList());
|
||||||
}
|
}
|
||||||
return isPublicKeyInList;
|
return isPublicKeyInList;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ public final class CryptoCurrency extends TradeCurrency {
|
||||||
|
|
||||||
public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) {
|
public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) {
|
||||||
return new CryptoCurrency(proto.getCode(),
|
return new CryptoCurrency(proto.getCode(),
|
||||||
proto.getName(),
|
CurrencyUtil.getNameByCode(proto.getCode()),
|
||||||
proto.getCryptoCurrency().getIsAsset());
|
proto.getCryptoCurrency().getIsAsset());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ import static java.lang.String.format;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CurrencyUtil {
|
public class CurrencyUtil {
|
||||||
public static void setup() {
|
public static void setup() {
|
||||||
setBaseCurrencyCode("XMR");
|
setBaseCurrencyCode(baseCurrencyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final AssetRegistry assetRegistry = new AssetRegistry();
|
private static final AssetRegistry assetRegistry = new AssetRegistry();
|
||||||
|
@ -198,12 +198,16 @@ public class CurrencyUtil {
|
||||||
final List<CryptoCurrency> result = new ArrayList<>();
|
final List<CryptoCurrency> result = new ArrayList<>();
|
||||||
result.add(new CryptoCurrency("BTC", "Bitcoin"));
|
result.add(new CryptoCurrency("BTC", "Bitcoin"));
|
||||||
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
|
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
|
||||||
|
result.add(new CryptoCurrency("DOGE", "Dogecoin"));
|
||||||
result.add(new CryptoCurrency("ETH", "Ether"));
|
result.add(new CryptoCurrency("ETH", "Ether"));
|
||||||
result.add(new CryptoCurrency("LTC", "Litecoin"));
|
result.add(new CryptoCurrency("LTC", "Litecoin"));
|
||||||
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)"));
|
result.add(new CryptoCurrency("XRP", "Ripple"));
|
||||||
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
|
result.add(new CryptoCurrency("ADA", "Cardano"));
|
||||||
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)"));
|
result.add(new CryptoCurrency("SOL", "Solana"));
|
||||||
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)"));
|
result.add(new CryptoCurrency("TRX", "Tron"));
|
||||||
|
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin"));
|
||||||
|
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD"));
|
||||||
|
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin"));
|
||||||
result.sort(TradeCurrency::compareTo);
|
result.sort(TradeCurrency::compareTo);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -284,7 +288,7 @@ public class CurrencyUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We return true if it is BTC or any of our currencies available in the assetRegistry.
|
* We return true if it is XMR or any of our currencies available in the assetRegistry.
|
||||||
* For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then.
|
* For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then.
|
||||||
* As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found
|
* As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found
|
||||||
* we return true as well in case we have no traditional currency for the code.
|
* we return true as well in case we have no traditional currency for the code.
|
||||||
|
@ -406,6 +410,13 @@ public class CurrencyUtil {
|
||||||
removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na");
|
removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na");
|
||||||
return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset);
|
return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset);
|
||||||
}
|
}
|
||||||
|
if (isTraditionalNonFiatCurrency(currencyCode)) {
|
||||||
|
return getTraditionalNonFiatCurrencies().stream()
|
||||||
|
.filter(currency -> currency.getCode().equals(currencyCode))
|
||||||
|
.findAny()
|
||||||
|
.map(TradeCurrency::getName)
|
||||||
|
.orElse(currencyCode);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return Currency.getInstance(currencyCode).getDisplayName();
|
return Currency.getInstance(currencyCode).getDisplayName();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
@ -507,17 +518,11 @@ public class CurrencyUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrencyPair(String currencyCode) {
|
public static String getCurrencyPair(String currencyCode) {
|
||||||
if (isTraditionalCurrency(currencyCode))
|
|
||||||
return Res.getBaseCurrencyCode() + "/" + currencyCode;
|
return Res.getBaseCurrencyCode() + "/" + currencyCode;
|
||||||
else
|
|
||||||
return currencyCode + "/" + Res.getBaseCurrencyCode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCounterCurrency(String currencyCode) {
|
public static String getCounterCurrency(String currencyCode) {
|
||||||
if (isTraditionalCurrency(currencyCode))
|
|
||||||
return currencyCode;
|
return currencyCode;
|
||||||
else
|
|
||||||
return Res.getBaseCurrencyCode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPriceWithCurrencyCode(String currencyCode) {
|
public static String getPriceWithCurrencyCode(String currencyCode) {
|
||||||
|
@ -525,9 +530,6 @@ public class CurrencyUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) {
|
public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) {
|
||||||
if (isCryptoCurrency(currencyCode))
|
|
||||||
return Res.get(translationKey, Res.getBaseCurrencyCode(), currencyCode);
|
|
||||||
else
|
|
||||||
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
|
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,11 @@ public class Res {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String get(String key, Object... arguments) {
|
public static String get(String key, Object... arguments) {
|
||||||
return MessageFormat.format(Res.get(key), arguments);
|
return MessageFormat.format(escapeQuotes(get(key)), arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeQuotes(String s) {
|
||||||
|
return s.replace("'", "''");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String get(String key) {
|
public static String get(String key) {
|
||||||
|
|
|
@ -86,7 +86,7 @@ public final class TraditionalCurrency extends TradeCurrency {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) {
|
public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) {
|
||||||
return new TraditionalCurrency(proto.getCode(), proto.getName());
|
return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class CryptoExchangeRate {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public final Coin coin;
|
public final Coin coin;
|
||||||
public final CryptoMoney crypto;
|
public final CryptoMoney cryptoMoney;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct exchange rate. This amount of coin is worth that amount of crypto.
|
* Construct exchange rate. This amount of coin is worth that amount of crypto.
|
||||||
|
@ -43,7 +43,7 @@ public class CryptoExchangeRate {
|
||||||
checkArgument(crypto.isPositive());
|
checkArgument(crypto.isPositive());
|
||||||
checkArgument(crypto.currencyCode != null, "currency code required");
|
checkArgument(crypto.currencyCode != null, "currency code required");
|
||||||
this.coin = coin;
|
this.coin = coin;
|
||||||
this.crypto = crypto;
|
this.cryptoMoney = crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,13 +59,13 @@ public class CryptoExchangeRate {
|
||||||
* @throws ArithmeticException if the converted crypto amount is too high or too low.
|
* @throws ArithmeticException if the converted crypto amount is too high or too low.
|
||||||
*/
|
*/
|
||||||
public CryptoMoney coinToCrypto(Coin convertCoin) {
|
public CryptoMoney coinToCrypto(Coin convertCoin) {
|
||||||
BigInteger converted = BigInteger.valueOf(coin.value)
|
final BigInteger converted = BigInteger.valueOf(convertCoin.value)
|
||||||
.multiply(BigInteger.valueOf(convertCoin.value))
|
.multiply(BigInteger.valueOf(cryptoMoney.value))
|
||||||
.divide(BigInteger.valueOf(crypto.value));
|
.divide(BigInteger.valueOf(coin.value));
|
||||||
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
||||||
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
||||||
throw new ArithmeticException("Overflow");
|
throw new ArithmeticException("Overflow");
|
||||||
return CryptoMoney.valueOf(crypto.currencyCode, converted.longValue());
|
return CryptoMoney.valueOf(cryptoMoney.currencyCode, converted.longValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,12 +74,11 @@ public class CryptoExchangeRate {
|
||||||
* @throws ArithmeticException if the converted coin amount is too high or too low.
|
* @throws ArithmeticException if the converted coin amount is too high or too low.
|
||||||
*/
|
*/
|
||||||
public Coin cryptoToCoin(CryptoMoney convertCrypto) {
|
public Coin cryptoToCoin(CryptoMoney convertCrypto) {
|
||||||
checkArgument(convertCrypto.currencyCode.equals(crypto.currencyCode), "Currency mismatch: %s vs %s",
|
checkArgument(convertCrypto.currencyCode.equals(cryptoMoney.currencyCode), "Currency mismatch: %s vs %s",
|
||||||
convertCrypto.currencyCode, crypto.currencyCode);
|
convertCrypto.currencyCode, cryptoMoney.currencyCode);
|
||||||
// Use BigInteger because it's much easier to maintain full precision without overflowing.
|
// Use BigInteger because it's much easier to maintain full precision without overflowing.
|
||||||
BigInteger converted = BigInteger.valueOf(crypto.value)
|
final BigInteger converted = BigInteger.valueOf(convertCrypto.value).multiply(BigInteger.valueOf(coin.value))
|
||||||
.multiply(BigInteger.valueOf(convertCrypto.value))
|
.divide(BigInteger.valueOf(cryptoMoney.value));
|
||||||
.divide(BigInteger.valueOf(coin.value));
|
|
||||||
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
||||||
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
||||||
throw new ArithmeticException("Overflow");
|
throw new ArithmeticException("Overflow");
|
||||||
|
|
|
@ -136,7 +136,7 @@ public class Price extends MonetaryWrapper implements Comparable<Price> {
|
||||||
|
|
||||||
public String toFriendlyString() {
|
public String toFriendlyString() {
|
||||||
return monetary instanceof CryptoMoney ?
|
return monetary instanceof CryptoMoney ?
|
||||||
((CryptoMoney) monetary).toFriendlyString() + "/XMR" :
|
((CryptoMoney) monetary).toFriendlyString().replace(((CryptoMoney) monetary).currencyCode, "") + "XMR/" + ((CryptoMoney) monetary).currencyCode :
|
||||||
((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode;
|
((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,21 @@
|
||||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package haveno.core.monetary;
|
package haveno.core.monetary;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount.
|
* An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount.
|
||||||
*/
|
*/
|
||||||
public class TraditionalExchangeRate implements Serializable {
|
public class TraditionalExchangeRate implements Serializable {
|
||||||
|
|
||||||
public final Coin coin;
|
public final Coin coin;
|
||||||
public final TraditionalMoney traditionalMoney;
|
public final TraditionalMoney traditionalMoney;
|
||||||
|
@ -54,7 +54,8 @@
|
||||||
*/
|
*/
|
||||||
public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) {
|
public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) {
|
||||||
// Use BigInteger because it's much easier to maintain full precision without overflowing.
|
// Use BigInteger because it's much easier to maintain full precision without overflowing.
|
||||||
final BigInteger converted = BigInteger.valueOf(convertCoin.value).multiply(BigInteger.valueOf(traditionalMoney.value))
|
final BigInteger converted = BigInteger.valueOf(convertCoin.value)
|
||||||
|
.multiply(BigInteger.valueOf(traditionalMoney.value))
|
||||||
.divide(BigInteger.valueOf(coin.value));
|
.divide(BigInteger.valueOf(coin.value));
|
||||||
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
|
||||||
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
|| converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
|
||||||
|
@ -94,5 +95,4 @@
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode(coin, traditionalMoney);
|
return Objects.hashCode(coin, traditionalMoney);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,5 +23,6 @@ public enum MessageState {
|
||||||
ARRIVED,
|
ARRIVED,
|
||||||
STORED_IN_MAILBOX,
|
STORED_IN_MAILBOX,
|
||||||
ACKNOWLEDGED,
|
ACKNOWLEDGED,
|
||||||
FAILED
|
FAILED,
|
||||||
|
NACKED
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class TradeEvents {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTradePhaseListener(Trade trade) {
|
private void setTradePhaseListener(Trade trade) {
|
||||||
if (isInitialized) log.info("We got a new trade. id={}", trade.getId());
|
if (isInitialized) log.info("We got a new trade, tradeId={}", trade.getId(), "hasBuyerAsTakerWithoutDeposit=" + trade.getOffer().hasBuyerAsTakerWithoutDeposit());
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
|
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
String msg = null;
|
String msg = null;
|
||||||
|
@ -70,6 +70,7 @@ public class TradeEvents {
|
||||||
case DEPOSITS_PUBLISHED:
|
case DEPOSITS_PUBLISHED:
|
||||||
break;
|
break;
|
||||||
case DEPOSITS_UNLOCKED:
|
case DEPOSITS_UNLOCKED:
|
||||||
|
case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized?
|
||||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
||||||
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
|
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -110,13 +110,12 @@ public class MarketAlerts {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onOfferAdded(Offer offer) {
|
private void onOfferAdded(Offer offer) {
|
||||||
String currencyCode = offer.getCurrencyCode();
|
String currencyCode = offer.getCounterCurrencyCode();
|
||||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
||||||
Price offerPrice = offer.getPrice();
|
Price offerPrice = offer.getPrice();
|
||||||
if (marketPrice != null && offerPrice != null) {
|
if (marketPrice != null && offerPrice != null) {
|
||||||
boolean isSellOffer = offer.getDirection() == OfferDirection.SELL;
|
boolean isSellOffer = offer.getDirection() == OfferDirection.SELL;
|
||||||
String shortOfferId = offer.getShortId();
|
String shortOfferId = offer.getShortId();
|
||||||
boolean isTraditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
|
|
||||||
String alertId = getAlertId(offer);
|
String alertId = getAlertId(offer);
|
||||||
user.getMarketAlertFilters().stream()
|
user.getMarketAlertFilters().stream()
|
||||||
.filter(marketAlertFilter -> !offer.isMyOffer(keyRing))
|
.filter(marketAlertFilter -> !offer.isMyOffer(keyRing))
|
||||||
|
@ -133,9 +132,7 @@ public class MarketAlerts {
|
||||||
double offerPriceValue = offerPrice.getValue();
|
double offerPriceValue = offerPrice.getValue();
|
||||||
double ratio = offerPriceValue / marketPriceAsDouble;
|
double ratio = offerPriceValue / marketPriceAsDouble;
|
||||||
ratio = 1 - ratio;
|
ratio = 1 - ratio;
|
||||||
if (isTraditionalCurrency && isSellOffer)
|
if (isSellOffer)
|
||||||
ratio *= -1;
|
|
||||||
else if (!isTraditionalCurrency && !isSellOffer)
|
|
||||||
ratio *= -1;
|
ratio *= -1;
|
||||||
|
|
||||||
ratio = ratio * 10000;
|
ratio = ratio * 10000;
|
||||||
|
@ -148,7 +145,6 @@ public class MarketAlerts {
|
||||||
if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) {
|
if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) {
|
||||||
String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy");
|
String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy");
|
||||||
String marketDir;
|
String marketDir;
|
||||||
if (isTraditionalCurrency) {
|
|
||||||
if (isSellOffer) {
|
if (isSellOffer) {
|
||||||
marketDir = ratio > 0 ?
|
marketDir = ratio > 0 ?
|
||||||
Res.get("account.notifications.marketAlert.message.msg.above") :
|
Res.get("account.notifications.marketAlert.message.msg.above") :
|
||||||
|
@ -158,17 +154,6 @@ public class MarketAlerts {
|
||||||
Res.get("account.notifications.marketAlert.message.msg.above") :
|
Res.get("account.notifications.marketAlert.message.msg.above") :
|
||||||
Res.get("account.notifications.marketAlert.message.msg.below");
|
Res.get("account.notifications.marketAlert.message.msg.below");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (isSellOffer) {
|
|
||||||
marketDir = ratio < 0 ?
|
|
||||||
Res.get("account.notifications.marketAlert.message.msg.above") :
|
|
||||||
Res.get("account.notifications.marketAlert.message.msg.below");
|
|
||||||
} else {
|
|
||||||
marketDir = ratio > 0 ?
|
|
||||||
Res.get("account.notifications.marketAlert.message.msg.above") :
|
|
||||||
Res.get("account.notifications.marketAlert.message.msg.below");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ratio = Math.abs(ratio);
|
ratio = Math.abs(ratio);
|
||||||
String msg = Res.get("account.notifications.marketAlert.message.msg",
|
String msg = Res.get("account.notifications.marketAlert.message.msg",
|
||||||
|
|
|
@ -22,7 +22,6 @@ import com.google.inject.Singleton;
|
||||||
import haveno.common.app.Version;
|
import haveno.common.app.Version;
|
||||||
import haveno.common.crypto.PubKeyRingProvider;
|
import haveno.common.crypto.PubKeyRingProvider;
|
||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
import haveno.core.locale.CurrencyUtil;
|
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.monetary.Price;
|
import haveno.core.monetary.Price;
|
||||||
import haveno.core.payment.PaymentAccount;
|
import haveno.core.payment.PaymentAccount;
|
||||||
|
@ -35,6 +34,7 @@ import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import haveno.core.util.coin.CoinUtil;
|
import haveno.core.util.coin.CoinUtil;
|
||||||
|
import haveno.core.xmr.wallet.Restrictions;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
|
@ -92,7 +92,6 @@ public class CreateOfferService {
|
||||||
Version.VERSION.replace(".", "");
|
Version.VERSION.replace(".", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add trigger price?
|
|
||||||
public Offer createAndGetOffer(String offerId,
|
public Offer createAndGetOffer(String offerId,
|
||||||
OfferDirection direction,
|
OfferDirection direction,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
|
@ -134,10 +133,12 @@ public class CreateOfferService {
|
||||||
// must nullify empty string so contracts match
|
// must nullify empty string so contracts match
|
||||||
if ("".equals(extraInfo)) extraInfo = null;
|
if ("".equals(extraInfo)) extraInfo = null;
|
||||||
|
|
||||||
// verify buyer as taker security deposit
|
// verify config for private no deposit offers
|
||||||
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
|
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
|
||||||
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
|
if (buyerAsTakerWithoutDeposit || isPrivateOffer) {
|
||||||
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
|
if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit");
|
||||||
|
if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers");
|
||||||
|
if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit");
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify fixed price xor market price with margin
|
// verify fixed price xor market price with margin
|
||||||
|
@ -149,15 +150,16 @@ public class CreateOfferService {
|
||||||
// verify price
|
// verify price
|
||||||
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
||||||
useMarketBasedPrice &&
|
useMarketBasedPrice &&
|
||||||
isMarketPriceAvailable(currencyCode) &&
|
isExternalPriceAvailable(currencyCode) &&
|
||||||
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
||||||
if (fixedPrice == null && !useMarketBasedPriceValue) {
|
if (fixedPrice == null && !useMarketBasedPriceValue) {
|
||||||
throw new IllegalArgumentException("Must provide fixed price");
|
throw new IllegalArgumentException("Must provide fixed price");
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust amount and min amount
|
// adjust amount and min amount
|
||||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit);
|
||||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||||
|
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||||
|
|
||||||
// generate one-time challenge for private offer
|
// generate one-time challenge for private offer
|
||||||
String challenge = null;
|
String challenge = null;
|
||||||
|
@ -173,16 +175,15 @@ public class CreateOfferService {
|
||||||
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
||||||
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
||||||
long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L;
|
long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L;
|
||||||
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
|
String baseCurrencyCode = Res.getBaseCurrencyCode();
|
||||||
String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode();
|
String counterCurrencyCode = currencyCode;
|
||||||
String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
|
|
||||||
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
|
String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount);
|
||||||
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
|
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
|
||||||
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
|
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
|
||||||
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
|
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
|
||||||
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
|
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
|
||||||
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
|
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
|
||||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
|
long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact();
|
||||||
boolean useAutoClose = false;
|
boolean useAutoClose = false;
|
||||||
boolean useReOpenAfterAutoClose = false;
|
boolean useReOpenAfterAutoClose = false;
|
||||||
long lowerClosePrice = 0;
|
long lowerClosePrice = 0;
|
||||||
|
@ -204,8 +205,8 @@ public class CreateOfferService {
|
||||||
useMarketBasedPriceValue,
|
useMarketBasedPriceValue,
|
||||||
amountAsLong,
|
amountAsLong,
|
||||||
minAmountAsLong,
|
minAmountAsLong,
|
||||||
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
|
HavenoUtils.getMakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
|
||||||
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
|
HavenoUtils.getTakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit),
|
||||||
HavenoUtils.PENALTY_FEE_PCT,
|
HavenoUtils.PENALTY_FEE_PCT,
|
||||||
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
|
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
|
||||||
securityDepositPct,
|
securityDepositPct,
|
||||||
|
@ -219,7 +220,7 @@ public class CreateOfferService {
|
||||||
acceptedBanks,
|
acceptedBanks,
|
||||||
Version.VERSION,
|
Version.VERSION,
|
||||||
xmrWalletService.getHeight(),
|
xmrWalletService.getHeight(),
|
||||||
maxTradeLimit,
|
maxTradeLimitAsLong,
|
||||||
maxTradePeriod,
|
maxTradePeriod,
|
||||||
useAutoClose,
|
useAutoClose,
|
||||||
useReOpenAfterAutoClose,
|
useReOpenAfterAutoClose,
|
||||||
|
@ -239,7 +240,6 @@ public class CreateOfferService {
|
||||||
return offer;
|
return offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add trigger price?
|
|
||||||
public Offer createClonedOffer(Offer sourceOffer,
|
public Offer createClonedOffer(Offer sourceOffer,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
Price fixedPrice,
|
Price fixedPrice,
|
||||||
|
@ -336,7 +336,7 @@ public class CreateOfferService {
|
||||||
// Private
|
// Private
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private boolean isMarketPriceAvailable(String currencyCode) {
|
private boolean isExternalPriceAvailable(String currencyCode) {
|
||||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
||||||
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
|
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,9 @@ import haveno.core.payment.payload.PaymentMethod;
|
||||||
import haveno.core.provider.price.MarketPrice;
|
import haveno.core.provider.price.MarketPrice;
|
||||||
import haveno.core.provider.price.PriceFeedService;
|
import haveno.core.provider.price.PriceFeedService;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
|
import haveno.core.util.PriceUtil;
|
||||||
import haveno.core.util.VolumeUtil;
|
import haveno.core.util.VolumeUtil;
|
||||||
|
import haveno.core.util.coin.CoinUtil;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
|
@ -173,32 +175,27 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Price getPrice() {
|
public Price getPrice() {
|
||||||
String currencyCode = getCurrencyCode();
|
String counterCurrencyCode = getCounterCurrencyCode();
|
||||||
if (!offerPayload.isUseMarketBasedPrice()) {
|
if (!offerPayload.isUseMarketBasedPrice()) {
|
||||||
return Price.valueOf(currencyCode, offerPayload.getPrice());
|
return Price.valueOf(counterCurrencyCode, isInverted() ? PriceUtil.invertLongPrice(offerPayload.getPrice(), counterCurrencyCode) : offerPayload.getPrice());
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNotNull(priceFeedService, "priceFeed must not be null");
|
checkNotNull(priceFeedService, "priceFeed must not be null");
|
||||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
MarketPrice marketPrice = priceFeedService.getMarketPrice(counterCurrencyCode);
|
||||||
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
|
if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) {
|
||||||
double factor;
|
double factor;
|
||||||
double marketPriceMargin = offerPayload.getMarketPriceMarginPct();
|
double marketPriceMargin = offerPayload.getMarketPriceMarginPct();
|
||||||
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
|
|
||||||
factor = getDirection() == OfferDirection.SELL ?
|
|
||||||
1 - marketPriceMargin : 1 + marketPriceMargin;
|
|
||||||
} else {
|
|
||||||
factor = getDirection() == OfferDirection.BUY ?
|
factor = getDirection() == OfferDirection.BUY ?
|
||||||
1 - marketPriceMargin : 1 + marketPriceMargin;
|
1 - marketPriceMargin : 1 + marketPriceMargin;
|
||||||
}
|
|
||||||
double marketPriceAsDouble = marketPrice.getPrice();
|
double marketPriceAsDouble = marketPrice.getPrice();
|
||||||
double targetPriceAsDouble = marketPriceAsDouble * factor;
|
double targetPriceAsDouble = marketPriceAsDouble * factor;
|
||||||
try {
|
try {
|
||||||
int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
|
int precision = CurrencyUtil.isTraditionalCurrency(counterCurrencyCode) ?
|
||||||
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
||||||
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
||||||
double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision);
|
double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision);
|
||||||
final long roundedToLong = MathUtils.roundDoubleToLong(scaled);
|
final long roundedToLong = MathUtils.roundDoubleToLong(scaled);
|
||||||
return Price.valueOf(currencyCode, roundedToLong);
|
return Price.valueOf(counterCurrencyCode, roundedToLong);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Exception at getPrice / parseToFiat: " + e + "\n" +
|
log.error("Exception at getPrice / parseToFiat: " + e + "\n" +
|
||||||
"That case should never happen.");
|
"That case should never happen.");
|
||||||
|
@ -224,7 +221,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Price tradePrice = Price.valueOf(getCurrencyCode(), price);
|
Price tradePrice = Price.valueOf(getCounterCurrencyCode(), price);
|
||||||
Price offerPrice = getPrice();
|
Price offerPrice = getPrice();
|
||||||
if (offerPrice == null)
|
if (offerPrice == null)
|
||||||
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
|
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
|
||||||
|
@ -239,7 +236,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
|
|
||||||
double deviation = Math.abs(1 - relation);
|
double deviation = Math.abs(1 - relation);
|
||||||
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
|
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
|
||||||
getShortId(), getCurrencyCode(), price, offerPrice.getValue(),
|
getShortId(), getCounterCurrencyCode(), price, offerPrice.getValue(),
|
||||||
deviation * 100 + "%");
|
deviation * 100 + "%");
|
||||||
if (deviation > PRICE_TOLERANCE) {
|
if (deviation > PRICE_TOLERANCE) {
|
||||||
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
|
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
|
||||||
|
@ -251,12 +248,13 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Volume getVolumeByAmount(BigInteger amount) {
|
public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) {
|
||||||
Price price = getPrice();
|
Price price = getPrice();
|
||||||
if (price == null || amount == null) {
|
if (price == null || amount == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Volume volumeByAmount = price.getVolumeByAmount(amount);
|
BigInteger adjustedAmount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, getCounterCurrencyCode(), getPaymentMethodId());
|
||||||
|
Volume volumeByAmount = price.getVolumeByAmount(adjustedAmount);
|
||||||
volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId());
|
volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId());
|
||||||
|
|
||||||
return volumeByAmount;
|
return volumeByAmount;
|
||||||
|
@ -385,12 +383,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Volume getVolume() {
|
public Volume getVolume() {
|
||||||
return getVolumeByAmount(getAmount());
|
return getVolumeByAmount(getAmount(), getMinAmount(), getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Volume getMinVolume() {
|
public Volume getMinVolume() {
|
||||||
return getVolumeByAmount(getMinAmount());
|
return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBuyOffer() {
|
public boolean isBuyOffer() {
|
||||||
|
@ -507,23 +505,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
return offerPayload.getCountryCode();
|
return offerPayload.getCountryCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCurrencyCode() {
|
public String getBaseCurrencyCode() {
|
||||||
if (currencyCode != null) {
|
return isInverted() ? offerPayload.getCounterCurrencyCode() : offerPayload.getBaseCurrencyCode(); // legacy offers inverted crypto
|
||||||
return currencyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
currencyCode = offerPayload.getBaseCurrencyCode().equals("XMR") ?
|
|
||||||
offerPayload.getCounterCurrencyCode() :
|
|
||||||
offerPayload.getBaseCurrencyCode();
|
|
||||||
return currencyCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCounterCurrencyCode() {
|
public String getCounterCurrencyCode() {
|
||||||
return offerPayload.getCounterCurrencyCode();
|
if (currencyCode != null) return currencyCode;
|
||||||
|
currencyCode = isInverted() ? offerPayload.getBaseCurrencyCode() : offerPayload.getCounterCurrencyCode(); // legacy offers inverted crypto
|
||||||
|
return currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBaseCurrencyCode() {
|
public boolean isInverted() {
|
||||||
return offerPayload.getBaseCurrencyCode();
|
return !offerPayload.getBaseCurrencyCode().equals("XMR");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPaymentMethodId() {
|
public String getPaymentMethodId() {
|
||||||
|
@ -584,21 +577,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||||
return offerPayload.isUseReOpenAfterAutoClose();
|
return offerPayload.isUseReOpenAfterAutoClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isXmrAutoConf() {
|
|
||||||
if (!isXmr()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (getExtraDataMap() == null || !getExtraDataMap().containsKey(OfferPayload.XMR_AUTO_CONF)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getExtraDataMap().get(OfferPayload.XMR_AUTO_CONF).equals(OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isXmr() {
|
|
||||||
return getCurrencyCode().equals("XMR");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTraditionalOffer() {
|
public boolean isTraditionalOffer() {
|
||||||
return CurrencyUtil.isTraditionalCurrency(currencyCode);
|
return CurrencyUtil.isTraditionalCurrency(currencyCode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,20 @@ public class OfferBookService {
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
announceOfferRemoved(offer);
|
announceOfferRemoved(offer);
|
||||||
|
|
||||||
|
// check if invalid offers are now valid
|
||||||
|
synchronized (invalidOffers) {
|
||||||
|
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
|
||||||
|
try {
|
||||||
|
validateOfferPayload(invalidOffer.getOfferPayload());
|
||||||
|
removeInvalidOffer(invalidOffer.getId());
|
||||||
|
replaceValidOffer(invalidOffer);
|
||||||
|
announceOfferAdded(invalidOffer);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, OfferBookService.class.getSimpleName());
|
}, OfferBookService.class.getSimpleName());
|
||||||
|
@ -257,7 +271,7 @@ public class OfferBookService {
|
||||||
|
|
||||||
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
|
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
|
||||||
return getOffers().stream()
|
return getOffers().stream()
|
||||||
.filter(o -> o.getOfferPayload().getBaseCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction)
|
.filter(o -> o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,20 +312,6 @@ public class OfferBookService {
|
||||||
synchronized (offerBookChangedListeners) {
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
|
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if invalid offers are now valid
|
|
||||||
synchronized (invalidOffers) {
|
|
||||||
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
|
|
||||||
try {
|
|
||||||
validateOfferPayload(invalidOffer.getOfferPayload());
|
|
||||||
removeInvalidOffer(invalidOffer.getId());
|
|
||||||
replaceValidOffer(invalidOffer);
|
|
||||||
announceOfferAdded(invalidOffer);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasValidOffer(String offerId) {
|
private boolean hasValidOffer(String offerId) {
|
||||||
|
@ -404,7 +404,7 @@ public class OfferBookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate max offers with same key images
|
// validate max offers with same key images
|
||||||
if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId());
|
if (numOffersWithSharedKeyImages > Restrictions.getMaxOffersWithSharedFunds()) throw new RuntimeException("More than " + Restrictions.getMaxOffersWithSharedFunds() + " offers exist with same same key images as new offerId=" + offerPayload.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -445,11 +445,11 @@ public class OfferBookService {
|
||||||
// We filter the case that it is a MarketBasedPrice but the price is not available
|
// We filter the case that it is a MarketBasedPrice but the price is not available
|
||||||
// That should only be possible if the price feed provider is not available
|
// That should only be possible if the price feed provider is not available
|
||||||
final List<OfferForJson> offerForJsonList = getOffers().stream()
|
final List<OfferForJson> offerForJsonList = getOffers().stream()
|
||||||
.filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCurrencyCode()) != null)
|
.filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCounterCurrencyCode()) != null)
|
||||||
.map(offer -> {
|
.map(offer -> {
|
||||||
try {
|
try {
|
||||||
return new OfferForJson(offer.getDirection(),
|
return new OfferForJson(offer.getDirection(),
|
||||||
offer.getCurrencyCode(),
|
offer.getCounterCurrencyCode(),
|
||||||
offer.getMinAmount(),
|
offer.getMinAmount(),
|
||||||
offer.getAmount(),
|
offer.getAmount(),
|
||||||
offer.getPrice(),
|
offer.getPrice(),
|
||||||
|
|
|
@ -127,6 +127,9 @@ public class OfferFilterService {
|
||||||
if (isMyInsufficientTradeLimit(offer)) {
|
if (isMyInsufficientTradeLimit(offer)) {
|
||||||
return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT;
|
return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT;
|
||||||
}
|
}
|
||||||
|
if (!hasValidArbitrator(offer)) {
|
||||||
|
return Result.ARBITRATOR_NOT_VALIDATED;
|
||||||
|
}
|
||||||
if (!hasValidSignature(offer)) {
|
if (!hasValidSignature(offer)) {
|
||||||
return Result.SIGNATURE_NOT_VALIDATED;
|
return Result.SIGNATURE_NOT_VALIDATED;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +162,7 @@ public class OfferFilterService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrencyBanned(Offer offer) {
|
public boolean isCurrencyBanned(Offer offer) {
|
||||||
return filterManager.isCurrencyBanned(offer.getCurrencyCode());
|
return filterManager.isCurrencyBanned(offer.getCounterCurrencyCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPaymentMethodBanned(Offer offer) {
|
public boolean isPaymentMethodBanned(Offer offer) {
|
||||||
|
@ -201,7 +204,7 @@ public class OfferFilterService {
|
||||||
accountAgeWitnessService);
|
accountAgeWitnessService);
|
||||||
long myTradeLimit = accountOptional
|
long myTradeLimit = accountOptional
|
||||||
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||||
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
|
offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
|
||||||
.orElse(0L);
|
.orElse(0L);
|
||||||
long offerMinAmount = offer.getMinAmount().longValueExact();
|
long offerMinAmount = offer.getMinAmount().longValueExact();
|
||||||
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",
|
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",
|
||||||
|
@ -215,27 +218,28 @@ public class OfferFilterService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasValidSignature(Offer offer) {
|
private boolean hasValidArbitrator(Offer offer) {
|
||||||
|
Arbitrator arbitrator = getArbitrator(offer);
|
||||||
|
return arbitrator != null;
|
||||||
|
}
|
||||||
|
|
||||||
// get accepted arbitrator by address
|
private Arbitrator getArbitrator(Offer offer) {
|
||||||
|
|
||||||
|
// get arbitrator by address
|
||||||
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
||||||
|
if (arbitrator != null) return arbitrator;
|
||||||
|
|
||||||
// accepted arbitrator is null if we are the signing arbitrator
|
// check if we are the signing arbitrator
|
||||||
if (arbitrator == null && offer.getOfferPayload().getArbitratorSigner() != null) {
|
|
||||||
Arbitrator thisArbitrator = user.getRegisteredArbitrator();
|
Arbitrator thisArbitrator = user.getRegisteredArbitrator();
|
||||||
if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) {
|
if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) return thisArbitrator;
|
||||||
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// // otherwise log warning that arbitrator is unregistered
|
// cannot get arbitrator
|
||||||
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
return null;
|
||||||
// if (!arbitratorAddresses.isEmpty()) {
|
|
||||||
// log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arbitrator == null) return false; // invalid arbitrator
|
private boolean hasValidSignature(Offer offer) {
|
||||||
|
Arbitrator arbitrator = getArbitrator(offer);
|
||||||
|
if (arbitrator == null) return false;
|
||||||
return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator);
|
return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,14 +100,8 @@ public class OfferForJson {
|
||||||
private void setDisplayStrings() {
|
private void setDisplayStrings() {
|
||||||
try {
|
try {
|
||||||
final Price price = getPrice();
|
final Price price = getPrice();
|
||||||
|
|
||||||
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
|
|
||||||
primaryMarketDirection = direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY;
|
|
||||||
currencyPair = currencyCode + "/" + Res.getBaseCurrencyCode();
|
|
||||||
} else {
|
|
||||||
primaryMarketDirection = direction;
|
primaryMarketDirection = direction;
|
||||||
currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode;
|
currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode;
|
||||||
}
|
|
||||||
|
|
||||||
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
|
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
|
||||||
priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString();
|
priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString();
|
||||||
|
@ -116,7 +110,6 @@ public class OfferForJson {
|
||||||
primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString();
|
primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString();
|
||||||
primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString();
|
primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString();
|
||||||
} else {
|
} else {
|
||||||
// amount and volume is inverted for json
|
|
||||||
priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString();
|
priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString();
|
||||||
primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString();
|
primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString();
|
||||||
primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString();
|
primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString();
|
||||||
|
|
|
@ -56,12 +56,13 @@ import haveno.core.payment.PayPalAccount;
|
||||||
import haveno.core.payment.PaymentAccount;
|
import haveno.core.payment.PaymentAccount;
|
||||||
import haveno.core.provider.price.MarketPrice;
|
import haveno.core.provider.price.MarketPrice;
|
||||||
import haveno.core.provider.price.PriceFeedService;
|
import haveno.core.provider.price.PriceFeedService;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.statistics.ReferralIdService;
|
import haveno.core.trade.statistics.ReferralIdService;
|
||||||
import haveno.core.user.AutoConfirmSettings;
|
import haveno.core.user.AutoConfirmSettings;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
import haveno.core.util.coin.CoinFormatter;
|
||||||
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositPct;
|
||||||
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositPct;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -120,13 +121,13 @@ public class OfferUtil {
|
||||||
return direction == OfferDirection.BUY;
|
return direction == OfferDirection.BUY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getMaxTradeLimit(PaymentAccount paymentAccount,
|
public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
OfferDirection direction,
|
OfferDirection direction,
|
||||||
boolean buyerAsTakerWithoutDeposit) {
|
boolean buyerAsTakerWithoutDeposit) {
|
||||||
return paymentAccount != null
|
return BigInteger.valueOf(paymentAccount != null
|
||||||
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
|
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
|
||||||
: 0;
|
: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -239,12 +240,12 @@ public class OfferUtil {
|
||||||
PaymentAccount paymentAccount,
|
PaymentAccount paymentAccount,
|
||||||
String currencyCode) {
|
String currencyCode) {
|
||||||
checkNotNull(p2PService.getAddress(), "Address must not be null");
|
checkNotNull(p2PService.getAddress(), "Address must not be null");
|
||||||
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
|
checkArgument(securityDeposit <= getMaxSecurityDepositPct(),
|
||||||
"securityDeposit must not exceed " +
|
"securityDeposit must not exceed " +
|
||||||
getMaxSecurityDepositAsPercent());
|
getMaxSecurityDepositPct());
|
||||||
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
|
checkArgument(securityDeposit >= getMinSecurityDepositPct(),
|
||||||
"securityDeposit must not be less than " +
|
"securityDeposit must not be less than " +
|
||||||
getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
|
getMinSecurityDepositPct() + " but was " + securityDeposit);
|
||||||
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
|
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
|
||||||
Res.get("offerbook.warning.currencyBanned"));
|
Res.get("offerbook.warning.currencyBanned"));
|
||||||
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
|
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
|
||||||
|
@ -263,10 +264,27 @@ public class OfferUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isTraditionalOffer(Offer offer) {
|
public static boolean isTraditionalOffer(Offer offer) {
|
||||||
return offer.getBaseCurrencyCode().equals("XMR");
|
return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCryptoOffer(Offer offer) {
|
public static boolean isCryptoOffer(Offer offer) {
|
||||||
return offer.getCounterCurrencyCode().equals("XMR");
|
return CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount,
|
||||||
|
String currencyCode,
|
||||||
|
OfferDirection direction,
|
||||||
|
boolean buyerAsTakerWithoutDeposit) {
|
||||||
|
|
||||||
|
// disallow offers which no buyer can take due to trade limits on release
|
||||||
|
if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) {
|
||||||
|
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentAccount != null) {
|
||||||
|
return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit));
|
||||||
|
} else {
|
||||||
|
return BigInteger.ZERO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,16 +122,16 @@ public final class OpenOffer implements Tradable {
|
||||||
this(offer, 0, false);
|
this(offer, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OpenOffer(Offer offer, long triggerPrice) {
|
public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) {
|
||||||
this(offer, triggerPrice, false);
|
this(offer, triggerPrice, reserveExactAmount, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) {
|
public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount, String groupId) {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.triggerPrice = triggerPrice;
|
this.triggerPrice = triggerPrice;
|
||||||
this.reserveExactAmount = reserveExactAmount;
|
this.reserveExactAmount = reserveExactAmount;
|
||||||
this.challenge = offer.getChallenge();
|
this.challenge = offer.getChallenge();
|
||||||
this.groupId = UUID.randomUUID().toString();
|
this.groupId = groupId == null ? UUID.randomUUID().toString() : groupId;
|
||||||
state = State.PENDING;
|
state = State.PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable {
|
||||||
return state == State.AVAILABLE;
|
return state == State.AVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReserved() {
|
||||||
|
return state == State.RESERVED;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isDeactivated() {
|
public boolean isDeactivated() {
|
||||||
return state == State.DEACTIVATED;
|
return state == State.DEACTIVATED;
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import haveno.core.util.JsonUtil;
|
import haveno.core.util.JsonUtil;
|
||||||
|
import haveno.core.util.PriceUtil;
|
||||||
import haveno.core.util.Validator;
|
import haveno.core.util.Validator;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
|
@ -519,6 +520,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
|
|
||||||
|
// cannot set trigger price for fixed price offers
|
||||||
|
if (triggerPrice != 0 && offer.getOfferPayload().getPrice() != 0) {
|
||||||
|
errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check source offer and clone limit
|
// check source offer and clone limit
|
||||||
OpenOffer sourceOffer = null;
|
OpenOffer sourceOffer = null;
|
||||||
if (sourceOfferId != null) {
|
if (sourceOfferId != null) {
|
||||||
|
@ -526,15 +533,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
// get source offer
|
// get source offer
|
||||||
Optional<OpenOffer> sourceOfferOptional = getOpenOffer(sourceOfferId);
|
Optional<OpenOffer> sourceOfferOptional = getOpenOffer(sourceOfferId);
|
||||||
if (!sourceOfferOptional.isPresent()) {
|
if (!sourceOfferOptional.isPresent()) {
|
||||||
errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId);
|
errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sourceOffer = sourceOfferOptional.get();
|
sourceOffer = sourceOfferOptional.get();
|
||||||
|
|
||||||
// check clone limit
|
// check clone limit
|
||||||
int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size();
|
int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size();
|
||||||
if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) {
|
if (numClones >= Restrictions.getMaxOffersWithSharedFunds()) {
|
||||||
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached.");
|
errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.getMaxOffersWithSharedFunds() + " cloned offers with shared funds reached.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -632,7 +639,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
|
|
||||||
private void applyTriggerState(OpenOffer openOffer) {
|
private void applyTriggerState(OpenOffer openOffer) {
|
||||||
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
|
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
|
||||||
if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) {
|
if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer)) {
|
||||||
openOffer.deactivate(true);
|
openOffer.deactivate(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -661,7 +668,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
log.info("Canceling open offer: {}", openOffer.getId());
|
log.info("Canceling open offer: {}", openOffer.getId());
|
||||||
if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
||||||
if (openOffer.isAvailable()) {
|
if (isOnOfferBook(openOffer)) {
|
||||||
openOffer.setState(OpenOffer.State.CANCELED);
|
openOffer.setState(OpenOffer.State.CANCELED);
|
||||||
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
|
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -683,6 +690,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isOnOfferBook(OpenOffer openOffer) {
|
||||||
|
return openOffer.isAvailable() || openOffer.isReserved();
|
||||||
|
}
|
||||||
|
|
||||||
public void editOpenOfferStart(OpenOffer openOffer,
|
public void editOpenOfferStart(OpenOffer openOffer,
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
|
@ -1083,6 +1094,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
try {
|
try {
|
||||||
ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
|
ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
openOffer.getOffer().setState(Offer.State.INVALID);
|
||||||
errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
|
errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1101,6 +1113,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// validate non-pending state
|
// validate non-pending state
|
||||||
|
boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
|
||||||
|
if (!skipValidation) {
|
||||||
try {
|
try {
|
||||||
validateSignedState(openOffer);
|
validateSignedState(openOffer);
|
||||||
resultHandler.handleResult(null); // done processing if non-pending state is valid
|
resultHandler.handleResult(null); // done processing if non-pending state is valid
|
||||||
|
@ -1114,6 +1128,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
|
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sign and post offer if already funded
|
// sign and post offer if already funded
|
||||||
if (openOffer.getReserveTxHash() != null) {
|
if (openOffer.getReserveTxHash() != null) {
|
||||||
|
@ -1168,10 +1183,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
return;
|
return;
|
||||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||||
scheduleWithEarliestTxs(openOffers, openOffer);
|
scheduleWithEarliestTxs(openOffers, openOffer);
|
||||||
|
}
|
||||||
|
|
||||||
resultHandler.handleResult(null);
|
resultHandler.handleResult(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e);
|
if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e);
|
||||||
errorMessageHandler.handleErrorMessage(e.getMessage());
|
errorMessageHandler.handleErrorMessage(e.getMessage());
|
||||||
|
@ -1186,7 +1202,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
} else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) {
|
} else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) {
|
||||||
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature");
|
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature");
|
||||||
} else if (arbitrator == null) {
|
} else if (arbitrator == null) {
|
||||||
throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator");
|
throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unregistered arbitrator");
|
||||||
} else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) {
|
} else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) {
|
||||||
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature");
|
throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature");
|
||||||
} else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) {
|
} else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) {
|
||||||
|
@ -1208,6 +1224,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
|
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
|
||||||
|
|
||||||
// check if split output tx is available for offer
|
// check if split output tx is available for offer
|
||||||
|
if (splitOutputTx != null) {
|
||||||
if (splitOutputTx.isLocked()) return splitOutputTx;
|
if (splitOutputTx.isLocked()) return splitOutputTx;
|
||||||
else {
|
else {
|
||||||
boolean isAvailable = true;
|
boolean isAvailable = true;
|
||||||
|
@ -1218,7 +1235,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
|
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
|
||||||
else log.warn("Split output tx is no longer available for offer {}", openOffer.getId());
|
else log.warn("Split output tx {} is no longer available for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Split output tx {} no longer exists for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1244,7 +1264,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
|
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
|
||||||
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
|
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsFailed(false)); // TODO: not using setIsIncoming(true) because split output txs sent to self have false; fix in monero-java?
|
||||||
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
|
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
|
||||||
for (MoneroTxWallet tx : splitOutputTxs) {
|
for (MoneroTxWallet tx : splitOutputTxs) {
|
||||||
if (tx.getOutputs() != null) { // outputs not available until first confirmation
|
if (tx.getOutputs() != null) { // outputs not available until first confirmation
|
||||||
|
@ -1267,6 +1287,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
|
boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.setSubaddressIndex(preferredSubaddressIndex)
|
.setSubaddressIndex(preferredSubaddressIndex)
|
||||||
|
.setIsIncoming(true)
|
||||||
.setAmount(amount)).size() > 0);
|
.setAmount(amount)).size() > 0);
|
||||||
return hasExactTransfer;
|
return hasExactTransfer;
|
||||||
}
|
}
|
||||||
|
@ -1342,7 +1363,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
|
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
|
||||||
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
xmrWalletService.handleWalletError(e, sourceConnection);
|
xmrWalletService.handleWalletError(e, sourceConnection, i + 1);
|
||||||
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
|
@ -1549,8 +1570,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify max length of extra info
|
// verify max length of extra info
|
||||||
if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
|
if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.getMaxExtraInfoLength()) {
|
||||||
errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length();
|
errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.getMaxExtraInfoLength() + " but got " + offer.getOfferPayload().getExtraInfo().length();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
|
@ -1574,21 +1595,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify the max version number
|
|
||||||
if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) {
|
|
||||||
errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION;
|
|
||||||
log.warn(errorMessage);
|
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify maker and taker fees
|
// verify maker and taker fees
|
||||||
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
|
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
|
||||||
if (hasBuyerAsTakerWithoutDeposit) {
|
if (hasBuyerAsTakerWithoutDeposit) {
|
||||||
|
|
||||||
// verify maker's trade fee
|
// verify maker's trade fee
|
||||||
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) {
|
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
|
||||||
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct();
|
if (offer.getMakerFeePct() != makerFeePct) {
|
||||||
|
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
|
@ -1603,8 +1617,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify maker security deposit
|
// verify maker security deposit
|
||||||
if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
|
if (offer.getSellerSecurityDepositPct() != Restrictions.getMinSecurityDepositPct()) {
|
||||||
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
|
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
|
@ -1619,33 +1633,43 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
// verify public offer (remove to generally allow private offers)
|
||||||
|
if (offer.isPrivateOffer() || offer.getChallengeHash() != null) {
|
||||||
|
errorMessage = "Private offer " + request.offerId + " is not valid. It must have direction SELL, taker fee of 0, and a challenge hash.";
|
||||||
|
log.warn(errorMessage);
|
||||||
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// verify maker's trade fee
|
// verify maker's trade fee
|
||||||
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
|
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
|
||||||
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct();
|
if (offer.getMakerFeePct() != makerFeePct) {
|
||||||
|
errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify taker's trade fee
|
// verify taker's trade fee
|
||||||
if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
|
double takerFeePct = HavenoUtils.getTakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
|
||||||
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct();
|
if (offer.getTakerFeePct() != takerFeePct) {
|
||||||
|
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + takerFeePct + " but got " + offer.getTakerFeePct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify seller's security deposit
|
// verify seller's security deposit
|
||||||
if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
|
if (offer.getSellerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
|
||||||
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
|
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify buyer's security deposit
|
// verify buyer's security deposit
|
||||||
if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
|
if (offer.getBuyerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) {
|
||||||
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct();
|
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getBuyerSecurityDepositPct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
|
@ -1662,17 +1686,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
|
|
||||||
// verify penalty fee
|
// verify penalty fee
|
||||||
if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) {
|
if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) {
|
||||||
errorMessage = "Wrong penalty fee for offer " + request.offerId;
|
errorMessage = "Wrong penalty fee percent for offer " + request.offerId + ". Expected " + HavenoUtils.PENALTY_FEE_PCT + " but got " + offer.getPenaltyFeePct();
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
|
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
|
||||||
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT);
|
double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit);
|
||||||
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT);
|
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), makerFeePct);
|
||||||
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
|
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
|
||||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
|
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
|
||||||
|
BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, HavenoUtils.PENALTY_FEE_PCT);
|
||||||
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
|
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
|
||||||
offer.getId(),
|
offer.getId(),
|
||||||
penaltyFee,
|
penaltyFee,
|
||||||
|
@ -1696,7 +1721,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
signedOfferPayload.getPubKeyRing().hashCode(), // trader id
|
signedOfferPayload.getPubKeyRing().hashCode(), // trader id
|
||||||
signedOfferPayload.getId(),
|
signedOfferPayload.getId(),
|
||||||
offer.getAmount().longValueExact(),
|
offer.getAmount().longValueExact(),
|
||||||
maxTradeFee.longValueExact(),
|
penaltyFee.longValueExact(),
|
||||||
request.getReserveTxHash(),
|
request.getReserveTxHash(),
|
||||||
request.getReserveTxHex(),
|
request.getReserveTxHex(),
|
||||||
request.getReserveTxKeyImages(),
|
request.getReserveTxKeyImages(),
|
||||||
|
@ -1736,6 +1761,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
errorMessage = "Exception at handleSignOfferRequest " + e.getMessage();
|
errorMessage = "Exception at handleSignOfferRequest " + e.getMessage();
|
||||||
log.error(errorMessage + "\n", e);
|
log.error(errorMessage + "\n", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (result == false && errorMessage == null) {
|
||||||
|
log.warn("Arbitrator is NACKing SignOfferRequest for unknown reason with offerId={}. That should never happen", request.getOfferId());
|
||||||
|
log.warn("Printing stacktrace:");
|
||||||
|
Thread.dumpStack();
|
||||||
|
}
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1925,8 +1955,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
result,
|
result,
|
||||||
errorMessage);
|
errorMessage);
|
||||||
|
|
||||||
|
if (ackMessage.isSuccess()) {
|
||||||
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
|
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
|
||||||
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
|
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
|
||||||
|
} else {
|
||||||
|
log.warn("Sending NACK for {} to peer {} with offerId {} and sourceUid {}, errorMessage={}",
|
||||||
|
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid(), errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
p2PService.sendEncryptedDirectMessage(
|
p2PService.sendEncryptedDirectMessage(
|
||||||
sender,
|
sender,
|
||||||
senderPubKeyRing,
|
senderPubKeyRing,
|
||||||
|
@ -1952,8 +1988,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void maybeUpdatePersistedOffers() {
|
private void maybeUpdatePersistedOffers() {
|
||||||
List<OpenOffer> openOffersClone = getOpenOffers();
|
|
||||||
openOffersClone.forEach(originalOpenOffer -> {
|
// update open offers
|
||||||
|
List<OpenOffer> updatedOpenOffers = new ArrayList<>();
|
||||||
|
getOpenOffers().forEach(originalOpenOffer -> {
|
||||||
Offer originalOffer = originalOpenOffer.getOffer();
|
Offer originalOffer = originalOpenOffer.getOffer();
|
||||||
|
|
||||||
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
|
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
|
||||||
|
@ -2000,30 +2038,31 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId());
|
log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long normalizedPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOfferPayload.getPrice(), originalOffer.getCounterCurrencyCode()) : originalOfferPayload.getPrice();
|
||||||
OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(),
|
OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(),
|
||||||
originalOfferPayload.getDate(),
|
originalOfferPayload.getDate(),
|
||||||
ownerNodeAddress,
|
ownerNodeAddress,
|
||||||
originalOfferPayload.getPubKeyRing(),
|
originalOfferPayload.getPubKeyRing(),
|
||||||
originalOfferPayload.getDirection(),
|
originalOfferPayload.getDirection(),
|
||||||
originalOfferPayload.getPrice(),
|
normalizedPrice,
|
||||||
originalOfferPayload.getMarketPriceMarginPct(),
|
originalOfferPayload.getMarketPriceMarginPct(),
|
||||||
originalOfferPayload.isUseMarketBasedPrice(),
|
originalOfferPayload.isUseMarketBasedPrice(),
|
||||||
originalOfferPayload.getAmount(),
|
originalOfferPayload.getAmount(),
|
||||||
originalOfferPayload.getMinAmount(),
|
originalOfferPayload.getMinAmount(),
|
||||||
originalOfferPayload.getMakerFeePct(),
|
originalOfferPayload.getMakerFeePct(),
|
||||||
originalOfferPayload.getTakerFeePct(),
|
originalOfferPayload.getTakerFeePct(),
|
||||||
originalOfferPayload.getPenaltyFeePct(),
|
HavenoUtils.PENALTY_FEE_PCT,
|
||||||
originalOfferPayload.getBuyerSecurityDepositPct(),
|
originalOfferPayload.getBuyerSecurityDepositPct(),
|
||||||
originalOfferPayload.getSellerSecurityDepositPct(),
|
originalOfferPayload.getSellerSecurityDepositPct(),
|
||||||
originalOfferPayload.getBaseCurrencyCode(),
|
originalOffer.getBaseCurrencyCode(),
|
||||||
originalOfferPayload.getCounterCurrencyCode(),
|
originalOffer.getCounterCurrencyCode(),
|
||||||
originalOfferPayload.getPaymentMethodId(),
|
originalOfferPayload.getPaymentMethodId(),
|
||||||
originalOfferPayload.getMakerPaymentAccountId(),
|
originalOfferPayload.getMakerPaymentAccountId(),
|
||||||
originalOfferPayload.getCountryCode(),
|
originalOfferPayload.getCountryCode(),
|
||||||
originalOfferPayload.getAcceptedCountryCodes(),
|
originalOfferPayload.getAcceptedCountryCodes(),
|
||||||
originalOfferPayload.getBankId(),
|
originalOfferPayload.getBankId(),
|
||||||
originalOfferPayload.getAcceptedBankIds(),
|
originalOfferPayload.getAcceptedBankIds(),
|
||||||
originalOfferPayload.getVersionNr(),
|
Version.VERSION,
|
||||||
originalOfferPayload.getBlockHeightAtOfferCreation(),
|
originalOfferPayload.getBlockHeightAtOfferCreation(),
|
||||||
originalOfferPayload.getMaxTradeLimit(),
|
originalOfferPayload.getMaxTradeLimit(),
|
||||||
originalOfferPayload.getMaxTradePeriod(),
|
originalOfferPayload.getMaxTradePeriod(),
|
||||||
|
@ -2047,13 +2086,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
// create new offer
|
// create new offer
|
||||||
Offer updatedOffer = new Offer(updatedPayload);
|
Offer updatedOffer = new Offer(updatedPayload);
|
||||||
updatedOffer.setPriceFeedService(priceFeedService);
|
updatedOffer.setPriceFeedService(priceFeedService);
|
||||||
|
long normalizedTriggerPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOpenOffer.getTriggerPrice(), originalOffer.getCounterCurrencyCode()) : originalOpenOffer.getTriggerPrice();
|
||||||
|
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, normalizedTriggerPrice, originalOpenOffer.isReserveExactAmount(), originalOpenOffer.getGroupId());
|
||||||
|
updatedOpenOffer.setChallenge(originalOpenOffer.getChallenge());
|
||||||
|
updatedOpenOffers.add(updatedOpenOffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
|
// add updated open offers
|
||||||
|
updatedOpenOffers.forEach(updatedOpenOffer -> {
|
||||||
addOpenOffer(updatedOpenOffer);
|
addOpenOffer(updatedOpenOffer);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
log.info("Updating offer completed. id={}", updatedOpenOffer.getId());
|
||||||
log.info("Updating offer completed. id={}", originalOffer.getId());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2183,6 +2227,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
if (periodicRefreshOffersTimer == null)
|
if (periodicRefreshOffersTimer == null)
|
||||||
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
|
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
|
log.info("Refreshing my open offers");
|
||||||
synchronized (openOffers.getList()) {
|
synchronized (openOffers.getList()) {
|
||||||
int size = openOffers.size();
|
int size = openOffers.size();
|
||||||
//we clone our list as openOffers might change during our delayed call
|
//we clone our list as openOffers might change during our delayed call
|
||||||
|
|
|
@ -102,7 +102,7 @@ public class TriggerPriceService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
|
||||||
boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
|
boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode);
|
||||||
int smallestUnitExponent = traditionalCurrency ?
|
int smallestUnitExponent = traditionalCurrency ?
|
||||||
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
||||||
|
@ -116,15 +116,13 @@ public class TriggerPriceService {
|
||||||
|
|
||||||
OfferDirection direction = openOffer.getOffer().getDirection();
|
OfferDirection direction = openOffer.getOffer().getDirection();
|
||||||
boolean isSellOffer = direction == OfferDirection.SELL;
|
boolean isSellOffer = direction == OfferDirection.SELL;
|
||||||
boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
|
return isSellOffer ?
|
||||||
boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency;
|
|
||||||
return condition ?
|
|
||||||
marketPriceAsLong < triggerPrice :
|
marketPriceAsLong < triggerPrice :
|
||||||
marketPriceAsLong > triggerPrice;
|
marketPriceAsLong > triggerPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
|
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
|
||||||
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
|
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
|
||||||
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
|
||||||
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
CryptoMoney.SMALLEST_UNIT_EXPONENT;
|
||||||
|
@ -162,11 +160,11 @@ public class TriggerPriceService {
|
||||||
|
|
||||||
private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) {
|
private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) {
|
||||||
openOffers.forEach(openOffer -> {
|
openOffers.forEach(openOffer -> {
|
||||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
|
||||||
openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>());
|
openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>());
|
||||||
openOffersByCurrency.get(currencyCode).add(openOffer);
|
openOffersByCurrency.get(currencyCode).add(openOffer);
|
||||||
|
|
||||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode());
|
MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode());
|
||||||
if (marketPrice != null) {
|
if (marketPrice != null) {
|
||||||
checkPriceThreshold(marketPrice, openOffer);
|
checkPriceThreshold(marketPrice, openOffer);
|
||||||
}
|
}
|
||||||
|
@ -175,7 +173,7 @@ public class TriggerPriceService {
|
||||||
|
|
||||||
private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
|
private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
|
||||||
openOffers.forEach(openOffer -> {
|
openOffers.forEach(openOffer -> {
|
||||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
String currencyCode = openOffer.getOffer().getCounterCurrencyCode();
|
||||||
if (openOffersByCurrency.containsKey(currencyCode)) {
|
if (openOffersByCurrency.containsKey(currencyCode)) {
|
||||||
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
|
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
|
||||||
set.remove(openOffer);
|
set.remove(openOffer);
|
||||||
|
|
|
@ -72,10 +72,10 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
model.getProtocol().startTimeoutTimer();
|
model.getProtocol().startTimeoutTimer();
|
||||||
|
|
||||||
// collect relevant info
|
// collect relevant info
|
||||||
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
|
|
||||||
BigInteger makerFee = offer.getMaxMakerFee();
|
BigInteger makerFee = offer.getMaxMakerFee();
|
||||||
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
|
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
|
||||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
|
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
|
||||||
|
BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct());
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
|
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
|
||||||
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
|
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
|
||||||
|
@ -100,7 +100,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
model.getXmrWalletService().handleWalletError(e, sourceConnection);
|
model.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1);
|
||||||
verifyPending();
|
verifyPending();
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
|
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -60,8 +61,16 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// create request for arbitrator to sign offer
|
// get payout address entry
|
||||||
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
String returnAddress;
|
||||||
|
Optional<XmrAddressEntry> addressEntryOpt = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||||
|
if (addressEntryOpt.isPresent()) returnAddress = addressEntryOpt.get().getAddressString();
|
||||||
|
else {
|
||||||
|
log.warn("Payout address entry found for unsigned offer {} is missing, creating anew", offer.getId());
|
||||||
|
returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// build sign offer request
|
||||||
SignOfferRequest request = new SignOfferRequest(
|
SignOfferRequest request = new SignOfferRequest(
|
||||||
offer.getId(),
|
offer.getId(),
|
||||||
P2PService.getMyNodeAddress(),
|
P2PService.getMyNodeAddress(),
|
||||||
|
|
|
@ -23,6 +23,7 @@ import haveno.core.account.witness.AccountAgeWitnessService;
|
||||||
import haveno.core.offer.Offer;
|
import haveno.core.offer.Offer;
|
||||||
import haveno.core.offer.OfferDirection;
|
import haveno.core.offer.OfferDirection;
|
||||||
import haveno.core.offer.placeoffer.PlaceOfferModel;
|
import haveno.core.offer.placeoffer.PlaceOfferModel;
|
||||||
|
import haveno.core.payment.PaymentAccount;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.messages.TradeMessage;
|
import haveno.core.trade.messages.TradeMessage;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
|
@ -96,7 +97,10 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||||
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
|
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
|
||||||
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
|
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
|
||||||
|
|
||||||
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
|
PaymentAccount paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId());
|
||||||
|
checkArgument(paymentAccount != null, "Payment account is null. makerPaymentAccountId=" + offer.getMakerPaymentAccountId());
|
||||||
|
|
||||||
|
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCounterCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
|
||||||
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
|
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
|
||||||
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
|
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
|
||||||
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
|
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
|
||||||
|
@ -108,7 +112,7 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||||
checkArgument(offer.getDate().getTime() > 0,
|
checkArgument(offer.getDate().getTime() > 0,
|
||||||
"Date must not be 0. date=" + offer.getDate().toString());
|
"Date must not be 0. date=" + offer.getDate().toString());
|
||||||
|
|
||||||
checkNotNull(offer.getCurrencyCode(), "Currency is null");
|
checkNotNull(offer.getCounterCurrencyCode(), "Currency is null");
|
||||||
checkNotNull(offer.getDirection(), "Direction is null");
|
checkNotNull(offer.getDirection(), "Direction is null");
|
||||||
checkNotNull(offer.getId(), "Id is null");
|
checkNotNull(offer.getId(), "Id is null");
|
||||||
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
|
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
|
||||||
|
|
|
@ -106,7 +106,7 @@ public class TakeOfferModel implements Model {
|
||||||
calculateTotalToPay();
|
calculateTotalToPay();
|
||||||
offer.resetState();
|
offer.resetState();
|
||||||
|
|
||||||
priceFeedService.setCurrencyCode(offer.getCurrencyCode());
|
priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -147,7 +147,7 @@ public class TakeOfferModel implements Model {
|
||||||
|
|
||||||
private long getMaxTradeLimit() {
|
private long getMaxTradeLimit() {
|
||||||
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||||
offer.getCurrencyCode(),
|
offer.getCounterCurrencyCode(),
|
||||||
offer.getMirroredDirection(),
|
offer.getMirroredDirection(),
|
||||||
offer.hasBuyerAsTakerWithoutDeposit());
|
offer.hasBuyerAsTakerWithoutDeposit());
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package haveno.core.payment;
|
||||||
|
|
||||||
import haveno.core.api.model.PaymentAccountFormField;
|
import haveno.core.api.model.PaymentAccountFormField;
|
||||||
import haveno.core.locale.TraditionalCurrency;
|
import haveno.core.locale.TraditionalCurrency;
|
||||||
|
import haveno.core.locale.BankUtil;
|
||||||
import haveno.core.locale.TradeCurrency;
|
import haveno.core.locale.TradeCurrency;
|
||||||
import haveno.core.payment.payload.AchTransferAccountPayload;
|
import haveno.core.payment.payload.AchTransferAccountPayload;
|
||||||
import haveno.core.payment.payload.BankAccountPayload;
|
import haveno.core.payment.payload.BankAccountPayload;
|
||||||
|
@ -34,6 +35,19 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_ADDRESS,
|
||||||
|
PaymentAccountFormField.FieldId.BANK_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.BRANCH_ID,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NR,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_TYPE,
|
||||||
|
PaymentAccountFormField.FieldId.COUNTRY,
|
||||||
|
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public AchTransferAccount() {
|
public AchTransferAccount() {
|
||||||
super(PaymentMethod.ACH_TRANSFER);
|
super(PaymentMethod.ACH_TRANSFER);
|
||||||
}
|
}
|
||||||
|
@ -79,6 +93,15 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
|
||||||
|
var field = super.getEmptyFormField(fieldId);
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.BRANCH_ID) field.setLabel(BankUtil.getBranchIdLabel("US"));
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.ACCOUNT_TYPE) field.setLabel(BankUtil.getAccountTypeLabel("US"));
|
||||||
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,13 @@ public final class AliPayAccount extends PaymentAccount {
|
||||||
new TraditionalCurrency("ZAR")
|
new TraditionalCurrency("ZAR")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NR,
|
||||||
|
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public AliPayAccount() {
|
public AliPayAccount() {
|
||||||
super(PaymentMethod.ALI_PAY);
|
super(PaymentMethod.ALI_PAY);
|
||||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
||||||
|
@ -77,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAccountNr(String accountNr) {
|
public void setAccountNr(String accountNr) {
|
||||||
|
|
|
@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
|
||||||
new TraditionalCurrency("USD")
|
new TraditionalCurrency("USD")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR,
|
||||||
|
PaymentAccountFormField.FieldId.COUNTRY,
|
||||||
|
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Country country;
|
private Country country;
|
||||||
|
|
||||||
|
@ -65,7 +73,7 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmailOrMobileNr() {
|
public String getEmailOrMobileNr() {
|
||||||
|
@ -97,4 +105,11 @@ public final class AmazonGiftCardAccount extends PaymentAccount {
|
||||||
private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() {
|
private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() {
|
||||||
return (AmazonGiftCardAccountPayload) paymentAccountPayload;
|
return (AmazonGiftCardAccountPayload) paymentAccountPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
|
||||||
|
var field = super.getEmptyFormField(fieldId);
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,12 +17,15 @@
|
||||||
|
|
||||||
package haveno.core.payment;
|
package haveno.core.payment;
|
||||||
|
|
||||||
|
import haveno.core.api.model.PaymentAccountForm;
|
||||||
import haveno.core.api.model.PaymentAccountFormField;
|
import haveno.core.api.model.PaymentAccountFormField;
|
||||||
import haveno.core.locale.TraditionalCurrency;
|
import haveno.core.locale.TraditionalCurrency;
|
||||||
import haveno.core.locale.TradeCurrency;
|
import haveno.core.locale.TradeCurrency;
|
||||||
import haveno.core.payment.payload.InteracETransferAccountPayload;
|
import haveno.core.payment.payload.InteracETransferAccountPayload;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.payment.payload.PaymentMethod;
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
|
import haveno.core.payment.validation.InteracETransferValidator;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@ -33,6 +36,15 @@ public final class InteracETransferAccount extends PaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD"));
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR,
|
||||||
|
PaymentAccountFormField.FieldId.QUESTION,
|
||||||
|
PaymentAccountFormField.FieldId.ANSWER,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public InteracETransferAccount() {
|
public InteracETransferAccount() {
|
||||||
super(PaymentMethod.INTERAC_E_TRANSFER);
|
super(PaymentMethod.INTERAC_E_TRANSFER);
|
||||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
||||||
|
@ -50,15 +62,15 @@ public final class InteracETransferAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEmail(String email) {
|
public void setEmail(String email) {
|
||||||
((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email);
|
((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmail() {
|
public String getEmail() {
|
||||||
return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail();
|
return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAnswer(String answer) {
|
public void setAnswer(String answer) {
|
||||||
|
@ -84,4 +96,19 @@ public final class InteracETransferAccount extends PaymentAccount {
|
||||||
public String getHolderName() {
|
public String getHolderName() {
|
||||||
return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName();
|
return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
|
||||||
|
InteracETransferValidator interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator;
|
||||||
|
switch (fieldId) {
|
||||||
|
case QUESTION:
|
||||||
|
processValidationResult(interacETransferValidator.questionValidator.validate(value));
|
||||||
|
break;
|
||||||
|
case ANSWER:
|
||||||
|
processValidationResult(interacETransferValidator.answerValidator.validate(value));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
super.validateFormField(form, fieldId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,7 @@
|
||||||
package haveno.core.payment;
|
package haveno.core.payment;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.inject.Inject;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.user.Preferences;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -47,13 +46,6 @@ import java.util.Map;
|
||||||
|
|
||||||
public class JapanBankData {
|
public class JapanBankData {
|
||||||
|
|
||||||
private static String userLanguage;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
JapanBankData(Preferences preferences) {
|
|
||||||
userLanguage = preferences.getUserLanguage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Returns the main list of ~500 banks in Japan with bank codes,
|
Returns the main list of ~500 banks in Japan with bank codes,
|
||||||
but since 90%+ of people will be using one of ~30 major banks,
|
but since 90%+ of people will be using one of ~30 major banks,
|
||||||
|
@ -793,7 +785,7 @@ public class JapanBankData {
|
||||||
// don't localize these strings into all languages,
|
// don't localize these strings into all languages,
|
||||||
// all we want is either Japanese or English here.
|
// all we want is either Japanese or English here.
|
||||||
public static String getString(String id) {
|
public static String getString(String id) {
|
||||||
boolean ja = userLanguage.equals("ja");
|
boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja");
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case "bank":
|
case "bank":
|
||||||
|
|
|
@ -435,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
processValidationResult(new LengthValidator(2, 100).validate(value));
|
processValidationResult(new LengthValidator(2, 100).validate(value));
|
||||||
break;
|
break;
|
||||||
case ACCOUNT_TYPE:
|
case ACCOUNT_TYPE:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
processValidationResult(new LengthValidator(2, 100).validate(value));
|
||||||
|
break;
|
||||||
case ANSWER:
|
case ANSWER:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case BANK_ACCOUNT_NAME:
|
case BANK_ACCOUNT_NAME:
|
||||||
|
@ -491,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
processValidationResult(new BICValidator().validate(value));
|
processValidationResult(new BICValidator().validate(value));
|
||||||
break;
|
break;
|
||||||
case BRANCH_ID:
|
case BRANCH_ID:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
processValidationResult(new LengthValidator(2, 34).validate(value));
|
||||||
|
break;
|
||||||
case CITY:
|
case CITY:
|
||||||
processValidationResult(new LengthValidator(2, 34).validate(value));
|
processValidationResult(new LengthValidator(2, 34).validate(value));
|
||||||
break;
|
break;
|
||||||
|
@ -518,7 +520,8 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
case EXTRA_INFO:
|
case EXTRA_INFO:
|
||||||
break;
|
break;
|
||||||
case HOLDER_ADDRESS:
|
case HOLDER_ADDRESS:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
processValidationResult(new LengthValidator(0, 100).validate(value));
|
||||||
|
break;
|
||||||
case HOLDER_EMAIL:
|
case HOLDER_EMAIL:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case HOLDER_NAME:
|
case HOLDER_NAME:
|
||||||
|
@ -616,16 +619,20 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
break;
|
break;
|
||||||
case ACCOUNT_NR:
|
case ACCOUNT_NR:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
field.setLabel("payment.accountNr");
|
field.setLabel(Res.get("payment.accountNr"));
|
||||||
break;
|
break;
|
||||||
case ACCOUNT_OWNER:
|
case ACCOUNT_OWNER:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
field.setLabel(Res.get("payment.account.owner"));
|
field.setLabel(Res.get("payment.account.owner"));
|
||||||
break;
|
break;
|
||||||
case ACCOUNT_TYPE:
|
case ACCOUNT_TYPE:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
|
||||||
|
field.setLabel(Res.get("payment.select.account"));
|
||||||
|
break;
|
||||||
case ANSWER:
|
case ANSWER:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
|
field.setLabel(Res.get("payment.answer"));
|
||||||
|
break;
|
||||||
case BANK_ACCOUNT_NAME:
|
case BANK_ACCOUNT_NAME:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
field.setLabel(Res.get("payment.account.owner"));
|
field.setLabel(Res.get("payment.account.owner"));
|
||||||
|
@ -668,11 +675,11 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
break;
|
break;
|
||||||
case BENEFICIARY_ACCOUNT_NR:
|
case BENEFICIARY_ACCOUNT_NR:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
field.setLabel(Res.get("payment.swift.account"));
|
field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift
|
||||||
break;
|
break;
|
||||||
case BENEFICIARY_ADDRESS:
|
case BENEFICIARY_ADDRESS:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
|
field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
|
||||||
field.setLabel(Res.get("payment.swift.address.beneficiary"));
|
field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift
|
||||||
break;
|
break;
|
||||||
case BENEFICIARY_CITY:
|
case BENEFICIARY_CITY:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
|
@ -691,7 +698,9 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
field.setLabel("BIC");
|
field.setLabel("BIC");
|
||||||
break;
|
break;
|
||||||
case BRANCH_ID:
|
case BRANCH_ID:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
|
//field.setLabel("Not implemented"); // expected to be overridden by subclasses
|
||||||
|
break;
|
||||||
case CITY:
|
case CITY:
|
||||||
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
field.setLabel(Res.get("payment.account.city"));
|
field.setLabel(Res.get("payment.account.city"));
|
||||||
|
@ -717,7 +726,9 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
field.setLabel(Res.get("payment.shared.optionalExtra"));
|
field.setLabel(Res.get("payment.shared.optionalExtra"));
|
||||||
break;
|
break;
|
||||||
case HOLDER_ADDRESS:
|
case HOLDER_ADDRESS:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.TEXTAREA);
|
||||||
|
field.setLabel(Res.get("payment.account.owner.address"));
|
||||||
|
break;
|
||||||
case HOLDER_EMAIL:
|
case HOLDER_EMAIL:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case HOLDER_NAME:
|
case HOLDER_NAME:
|
||||||
|
@ -755,7 +766,9 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
field.setLabel(Res.get("payment.swift.swiftCode.intermediary"));
|
field.setLabel(Res.get("payment.swift.swiftCode.intermediary"));
|
||||||
break;
|
break;
|
||||||
case MOBILE_NR:
|
case MOBILE_NR:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
|
field.setLabel(Res.get("payment.mobile"));
|
||||||
|
break;
|
||||||
case NATIONAL_ACCOUNT_ID:
|
case NATIONAL_ACCOUNT_ID:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case PAYID:
|
case PAYID:
|
||||||
|
@ -771,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||||
case PROMPT_PAY_ID:
|
case PROMPT_PAY_ID:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case QUESTION:
|
case QUESTION:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
field.setComponent(PaymentAccountFormField.Component.TEXT);
|
||||||
|
field.setLabel(Res.get("payment.secret"));
|
||||||
|
break;
|
||||||
case REQUIREMENTS:
|
case REQUIREMENTS:
|
||||||
throw new IllegalArgumentException("Not implemented");
|
throw new IllegalArgumentException("Not implemented");
|
||||||
case SALT:
|
case SALT:
|
||||||
|
|
|
@ -50,6 +50,7 @@ import static haveno.common.util.Utilities.decodeFromHex;
|
||||||
import static haveno.core.locale.CountryUtil.findCountryByCode;
|
import static haveno.core.locale.CountryUtil.findCountryByCode;
|
||||||
import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList;
|
import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList;
|
||||||
import static haveno.core.locale.CurrencyUtil.getTradeCurrency;
|
import static haveno.core.locale.CurrencyUtil.getTradeCurrency;
|
||||||
|
import static haveno.core.payment.payload.PaymentMethod.AMAZON_GIFT_CARD_ID;
|
||||||
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
|
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
|
@ -438,6 +439,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
|
||||||
// account.setSingleTradeCurrency(fiatCurrency);
|
// account.setSingleTradeCurrency(fiatCurrency);
|
||||||
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
|
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
|
||||||
((MoneyGramAccount) account).setCountry(country.get());
|
((MoneyGramAccount) account).setCountry(country.get());
|
||||||
|
} else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) {
|
||||||
|
((AmazonGiftCardAccount) account).setCountry(country.get());
|
||||||
} else {
|
} else {
|
||||||
String errMsg = format("cannot set the country on a %s",
|
String errMsg = format("cannot set the country on a %s",
|
||||||
paymentAccountType.getSimpleName());
|
paymentAccountType.getSimpleName());
|
||||||
|
|
|
@ -122,9 +122,9 @@ public class PaymentAccountUtil {
|
||||||
public static boolean isAmountValidForOffer(Offer offer,
|
public static boolean isAmountValidForOffer(Offer offer,
|
||||||
PaymentAccount paymentAccount,
|
PaymentAccount paymentAccount,
|
||||||
AccountAgeWitnessService accountAgeWitnessService) {
|
AccountAgeWitnessService accountAgeWitnessService) {
|
||||||
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
|
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode());
|
||||||
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||||
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
|
offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
|
||||||
return !hasChargebackRisk || hasValidAccountAgeWitness;
|
return !hasChargebackRisk || hasValidAccountAgeWitness;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class ReceiptPredicates {
|
||||||
.map(TradeCurrency::getCode)
|
.map(TradeCurrency::getCode)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
return codes.contains(offer.getCurrencyCode());
|
return codes.contains(offer.getCounterCurrencyCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) {
|
boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) {
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
|
|
||||||
package haveno.core.payment;
|
package haveno.core.payment;
|
||||||
|
|
||||||
|
import haveno.core.api.model.PaymentAccountForm;
|
||||||
import haveno.core.api.model.PaymentAccountFormField;
|
import haveno.core.api.model.PaymentAccountFormField;
|
||||||
import haveno.core.locale.TraditionalCurrency;
|
import haveno.core.locale.TraditionalCurrency;
|
||||||
import haveno.core.locale.TradeCurrency;
|
import haveno.core.locale.TradeCurrency;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.payment.payload.PaymentMethod;
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
import haveno.core.payment.payload.SwishAccountPayload;
|
import haveno.core.payment.payload.SwishAccountPayload;
|
||||||
|
import haveno.core.payment.validation.SwishValidator;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@ -33,6 +35,13 @@ public final class SwishAccount extends PaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK"));
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.MOBILE_NR,
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public SwishAccount() {
|
public SwishAccount() {
|
||||||
super(PaymentMethod.SWISH);
|
super(PaymentMethod.SWISH);
|
||||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
||||||
|
@ -50,7 +59,7 @@ public final class SwishAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMobileNr(String mobileNr) {
|
public void setMobileNr(String mobileNr) {
|
||||||
|
@ -68,4 +77,16 @@ public final class SwishAccount extends PaymentAccount {
|
||||||
public String getHolderName() {
|
public String getHolderName() {
|
||||||
return ((SwishAccountPayload) paymentAccountPayload).getHolderName();
|
return ((SwishAccountPayload) paymentAccountPayload).getHolderName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
|
||||||
|
switch (fieldId) {
|
||||||
|
case MOBILE_NR:
|
||||||
|
processValidationResult(new SwishValidator().validate(value));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
super.validateFormField(form, fieldId, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package haveno.core.payment;
|
||||||
|
|
||||||
import haveno.core.api.model.PaymentAccountFormField;
|
import haveno.core.api.model.PaymentAccountFormField;
|
||||||
import haveno.core.locale.TraditionalCurrency;
|
import haveno.core.locale.TraditionalCurrency;
|
||||||
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.locale.TradeCurrency;
|
import haveno.core.locale.TradeCurrency;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.payment.payload.PaymentMethod;
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
|
@ -33,6 +34,15 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.EMAIL,
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_ADDRESS,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.COUNTRY,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public TransferwiseUsdAccount() {
|
public TransferwiseUsdAccount() {
|
||||||
super(PaymentMethod.TRANSFERWISE_USD);
|
super(PaymentMethod.TRANSFERWISE_USD);
|
||||||
// this payment method is currently restricted to United States/USD
|
// this payment method is currently restricted to United States/USD
|
||||||
|
@ -61,11 +71,11 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBeneficiaryAddress(String address) {
|
public void setBeneficiaryAddress(String address) {
|
||||||
((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address);
|
((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBeneficiaryAddress() {
|
public String getBeneficiaryAddress() {
|
||||||
return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress();
|
return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,6 +100,13 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
|
||||||
|
var field = super.getEmptyFormField(fieldId);
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_ADDRESS) field.setLabel(field.getLabel() + " " + Res.get("payment.transferwiseUsd.address"));
|
||||||
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,13 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD"));
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.HOLDER_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.POSTAL_ADDRESS,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public USPostalMoneyOrderAccount() {
|
public USPostalMoneyOrderAccount() {
|
||||||
super(PaymentMethod.US_POSTAL_MONEY_ORDER);
|
super(PaymentMethod.US_POSTAL_MONEY_ORDER);
|
||||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
||||||
|
@ -50,7 +57,7 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPostalAddress(String postalAddress) {
|
public void setPostalAddress(String postalAddress) {
|
||||||
|
|
|
@ -38,6 +38,13 @@ public final class WeChatPayAccount extends PaymentAccount {
|
||||||
new TraditionalCurrency("GBP")
|
new TraditionalCurrency("GBP")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NR,
|
||||||
|
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
public WeChatPayAccount() {
|
public WeChatPayAccount() {
|
||||||
super(PaymentMethod.WECHAT_PAY);
|
super(PaymentMethod.WECHAT_PAY);
|
||||||
}
|
}
|
||||||
|
@ -54,7 +61,7 @@ public final class WeChatPayAccount extends PaymentAccount {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
public @NonNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
throw new RuntimeException("Not implemented");
|
return INPUT_FIELD_IDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAccountNr(String accountNr) {
|
public void setAccountNr(String accountNr) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ import java.util.Map;
|
||||||
@Getter
|
@Getter
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
|
public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
|
||||||
private String email = "";
|
private String emailOrMobileNr = "";
|
||||||
private String holderName = "";
|
private String holderName = "";
|
||||||
private String question = "";
|
private String question = "";
|
||||||
private String answer = "";
|
private String answer = "";
|
||||||
|
@ -52,7 +52,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
|
||||||
|
|
||||||
private InteracETransferAccountPayload(String paymentMethod,
|
private InteracETransferAccountPayload(String paymentMethod,
|
||||||
String id,
|
String id,
|
||||||
String email,
|
String emailOrMobileNr,
|
||||||
String holderName,
|
String holderName,
|
||||||
String question,
|
String question,
|
||||||
String answer,
|
String answer,
|
||||||
|
@ -62,7 +62,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
|
||||||
id,
|
id,
|
||||||
maxTradePeriod,
|
maxTradePeriod,
|
||||||
excludeFromJsonDataMap);
|
excludeFromJsonDataMap);
|
||||||
this.email = email;
|
this.emailOrMobileNr = emailOrMobileNr;
|
||||||
this.holderName = holderName;
|
this.holderName = holderName;
|
||||||
this.question = question;
|
this.question = question;
|
||||||
this.answer = answer;
|
this.answer = answer;
|
||||||
|
@ -72,7 +72,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
return getPaymentAccountPayloadBuilder()
|
return getPaymentAccountPayloadBuilder()
|
||||||
.setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder()
|
.setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder()
|
||||||
.setEmail(email)
|
.setEmailOrMobileNr(emailOrMobileNr)
|
||||||
.setHolderName(holderName)
|
.setHolderName(holderName)
|
||||||
.setQuestion(question)
|
.setQuestion(question)
|
||||||
.setAnswer(answer))
|
.setAnswer(answer))
|
||||||
|
@ -82,7 +82,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
|
||||||
public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
|
public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
|
||||||
return new InteracETransferAccountPayload(proto.getPaymentMethodId(),
|
return new InteracETransferAccountPayload(proto.getPaymentMethodId(),
|
||||||
proto.getId(),
|
proto.getId(),
|
||||||
proto.getInteracETransferAccountPayload().getEmail(),
|
proto.getInteracETransferAccountPayload().getEmailOrMobileNr(),
|
||||||
proto.getInteracETransferAccountPayload().getHolderName(),
|
proto.getInteracETransferAccountPayload().getHolderName(),
|
||||||
proto.getInteracETransferAccountPayload().getQuestion(),
|
proto.getInteracETransferAccountPayload().getQuestion(),
|
||||||
proto.getInteracETransferAccountPayload().getAnswer(),
|
proto.getInteracETransferAccountPayload().getAnswer(),
|
||||||
|
@ -98,21 +98,21 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload
|
||||||
@Override
|
@Override
|
||||||
public String getPaymentDetails() {
|
public String getPaymentDetails() {
|
||||||
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " +
|
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " +
|
||||||
Res.get("payment.email") + " " + email + ", " + Res.getWithCol("payment.secret") + " " +
|
Res.get("payment.email") + " " + emailOrMobileNr + ", " + Res.getWithCol("payment.secret") + " " +
|
||||||
question + ", " + Res.getWithCol("payment.answer") + " " + answer;
|
question + ", " + Res.getWithCol("payment.answer") + " " + answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPaymentDetailsForTradePopup() {
|
public String getPaymentDetailsForTradePopup() {
|
||||||
return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" +
|
return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" +
|
||||||
Res.getWithCol("payment.email") + " " + email + "\n" +
|
Res.getWithCol("payment.email") + " " + emailOrMobileNr + "\n" +
|
||||||
Res.getWithCol("payment.secret") + " " + question + "\n" +
|
Res.getWithCol("payment.secret") + " " + question + "\n" +
|
||||||
Res.getWithCol("payment.answer") + " " + answer;
|
Res.getWithCol("payment.answer") + " " + answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] getAgeWitnessInputData() {
|
public byte[] getAgeWitnessInputData() {
|
||||||
return super.getAgeWitnessInputData(ArrayUtils.addAll(email.getBytes(StandardCharsets.UTF_8),
|
return super.getAgeWitnessInputData(ArrayUtils.addAll(emailOrMobileNr.getBytes(StandardCharsets.UTF_8),
|
||||||
ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8),
|
ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8),
|
||||||
answer.getBytes(StandardCharsets.UTF_8))));
|
answer.getBytes(StandardCharsets.UTF_8))));
|
||||||
}
|
}
|
||||||
|
|
|
@ -369,7 +369,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||||
CASH_APP_ID,
|
CASH_APP_ID,
|
||||||
PAYPAL_ID,
|
PAYPAL_ID,
|
||||||
VENMO_ID,
|
VENMO_ID,
|
||||||
PAYSAFE_ID);
|
PAYSAFE_ID,
|
||||||
|
WECHAT_PAY_ID,
|
||||||
|
ALI_PAY_ID,
|
||||||
|
SWISH_ID,
|
||||||
|
TRANSFERWISE_USD_ID,
|
||||||
|
AMAZON_GIFT_CARD_ID,
|
||||||
|
ACH_TRANSFER_ID,
|
||||||
|
INTERAC_E_TRANSFER_ID,
|
||||||
|
US_POSTAL_MONEY_ORDER_ID);
|
||||||
return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList());
|
return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ import java.util.Map;
|
||||||
public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload {
|
public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload {
|
||||||
private String email = "";
|
private String email = "";
|
||||||
private String holderName = "";
|
private String holderName = "";
|
||||||
private String beneficiaryAddress = "";
|
private String holderAddress = "";
|
||||||
|
|
||||||
public TransferwiseUsdAccountPayload(String paymentMethod, String id) {
|
public TransferwiseUsdAccountPayload(String paymentMethod, String id) {
|
||||||
super(paymentMethod, id);
|
super(paymentMethod, id);
|
||||||
|
@ -51,7 +51,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
|
||||||
List<String> acceptedCountryCodes,
|
List<String> acceptedCountryCodes,
|
||||||
String email,
|
String email,
|
||||||
String holderName,
|
String holderName,
|
||||||
String beneficiaryAddress,
|
String holderAddress,
|
||||||
long maxTradePeriod,
|
long maxTradePeriod,
|
||||||
Map<String, String> excludeFromJsonDataMap) {
|
Map<String, String> excludeFromJsonDataMap) {
|
||||||
super(paymentMethod,
|
super(paymentMethod,
|
||||||
|
@ -63,7 +63,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
|
||||||
|
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.holderName = holderName;
|
this.holderName = holderName;
|
||||||
this.beneficiaryAddress = beneficiaryAddress;
|
this.holderAddress = holderAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -71,7 +71,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
|
||||||
protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder()
|
protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder()
|
||||||
.setEmail(email)
|
.setEmail(email)
|
||||||
.setHolderName(holderName)
|
.setHolderName(holderName)
|
||||||
.setBeneficiaryAddress(beneficiaryAddress);
|
.setHolderAddress(holderAddress);
|
||||||
final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder()
|
final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder()
|
||||||
.getCountryBasedPaymentAccountPayloadBuilder()
|
.getCountryBasedPaymentAccountPayloadBuilder()
|
||||||
.setTransferwiseUsdAccountPayload(builder);
|
.setTransferwiseUsdAccountPayload(builder);
|
||||||
|
@ -89,7 +89,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco
|
||||||
new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()),
|
new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()),
|
||||||
accountPayloadPB.getEmail(),
|
accountPayloadPB.getEmail(),
|
||||||
accountPayloadPB.getHolderName(),
|
accountPayloadPB.getHolderName(),
|
||||||
accountPayloadPB.getBeneficiaryAddress(),
|
accountPayloadPB.getHolderAddress(),
|
||||||
proto.getMaxTradePeriod(),
|
proto.getMaxTradePeriod(),
|
||||||
new HashMap<>(proto.getExcludeFromJsonDataMap()));
|
new HashMap<>(proto.getExcludeFromJsonDataMap()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator {
|
||||||
private ValidationResult validateIfNotTooLowPercentageValue(String input) {
|
private ValidationResult validateIfNotTooLowPercentageValue(String input) {
|
||||||
try {
|
try {
|
||||||
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
||||||
double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
|
double minPercentage = Restrictions.getMinSecurityDepositPct();
|
||||||
if (percentage < minPercentage)
|
if (percentage < minPercentage)
|
||||||
return new ValidationResult(false,
|
return new ValidationResult(false,
|
||||||
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
|
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
|
||||||
|
@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator {
|
||||||
private ValidationResult validateIfNotTooHighPercentageValue(String input) {
|
private ValidationResult validateIfNotTooHighPercentageValue(String input) {
|
||||||
try {
|
try {
|
||||||
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
||||||
double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
|
double maxPercentage = Restrictions.getMaxSecurityDepositPct();
|
||||||
if (percentage > maxPercentage)
|
if (percentage > maxPercentage)
|
||||||
return new ValidationResult(false,
|
return new ValidationResult(false,
|
||||||
Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage)));
|
Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage)));
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class ProvidersRepository {
|
||||||
private static final List<String> DEFAULT_NODES = Arrays.asList(
|
private static final List<String> DEFAULT_NODES = Arrays.asList(
|
||||||
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
|
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
|
||||||
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake
|
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake
|
||||||
"http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck
|
"http://agorise7ae5g7lkqp7r7qddsyzskft7cqhgguwkadbqamtsrap5onead.onion/" // Agorise
|
||||||
);
|
);
|
||||||
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
|
|
|
@ -296,13 +296,13 @@ public class PriceFeedService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setHavenoMarketPrice(String currencyCode, Price price) {
|
private void setHavenoMarketPrice(String counterCurrencyCode, Price price) {
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode);
|
String counterCurrencyCodeBase = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode);
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
if (!cache.containsKey(currencyCodeBase) || !cache.get(currencyCodeBase).isExternallyProvidedPrice()) {
|
if (!cache.containsKey(counterCurrencyCodeBase) || !cache.get(counterCurrencyCodeBase).isExternallyProvidedPrice()) {
|
||||||
cache.put(currencyCodeBase, new MarketPrice(currencyCodeBase,
|
cache.put(counterCurrencyCodeBase, new MarketPrice(counterCurrencyCodeBase,
|
||||||
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
|
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(counterCurrencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
|
||||||
0,
|
0,
|
||||||
false));
|
false));
|
||||||
}
|
}
|
||||||
|
@ -371,9 +371,7 @@ public class PriceFeedService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns prices for all available currencies.
|
* Returns prices for all available currencies. The base currency is always XMR.
|
||||||
* For crypto currencies the value is XMR price for 1 unit of given crypto currency (e.g. 1 DOGE = X XMR).
|
|
||||||
* For traditional currencies the value is price in the given traditional currency per 1 XMR (e.g. 1 XMR = X USD).
|
|
||||||
*
|
*
|
||||||
* TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout
|
* TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -28,6 +28,8 @@ import haveno.network.p2p.P2PService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -63,16 +65,20 @@ public class PriceProvider extends HttpClientProvider {
|
||||||
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
|
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
|
||||||
String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode");
|
String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode");
|
||||||
String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode");
|
String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode");
|
||||||
String currencyCode = baseCurrencyCode.equals("XMR") ? counterCurrencyCode : baseCurrencyCode;
|
boolean isInverted = !"XMR".equalsIgnoreCase(baseCurrencyCode);
|
||||||
currencyCode = CurrencyUtil.getCurrencyCodeBase(currencyCode);
|
if (isInverted) {
|
||||||
|
String temp = baseCurrencyCode;
|
||||||
|
baseCurrencyCode = counterCurrencyCode;
|
||||||
|
counterCurrencyCode = temp;
|
||||||
|
}
|
||||||
|
counterCurrencyCode = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode);
|
||||||
double price = (Double) treeMap.get("price");
|
double price = (Double) treeMap.get("price");
|
||||||
// json uses double for our timestampSec long value...
|
if (isInverted) price = BigDecimal.ONE.divide(BigDecimal.valueOf(price), 10, RoundingMode.HALF_UP).doubleValue(); // XMR is always base currency, so invert price if applicable
|
||||||
long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));
|
long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));
|
||||||
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
|
marketPriceMap.put(counterCurrencyCode, new MarketPrice(counterCurrencyCode, price, timestampSec, true));
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
log.error("Error getting all prices: {}\n", t.getMessage(), t);
|
log.error("Error getting all prices: {}\n", t.getMessage(), t);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
return marketPriceMap;
|
return marketPriceMap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,15 +154,15 @@ public abstract class SupportManager {
|
||||||
// Message handler
|
// Message handler
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected void handleChatMessage(ChatMessage chatMessage) {
|
protected void handle(ChatMessage chatMessage) {
|
||||||
final String tradeId = chatMessage.getTradeId();
|
final String tradeId = chatMessage.getTradeId();
|
||||||
final String uid = chatMessage.getUid();
|
final String uid = chatMessage.getUid();
|
||||||
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
||||||
boolean channelOpen = channelOpen(chatMessage);
|
boolean channelOpen = channelOpen(chatMessage);
|
||||||
if (!channelOpen) {
|
if (!channelOpen) {
|
||||||
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
log.warn("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1);
|
Timer timer = UserThread.runAfter(() -> handle(chatMessage), 1);
|
||||||
delayMsgMap.put(uid, timer);
|
delayMsgMap.put(uid, timer);
|
||||||
} else {
|
} else {
|
||||||
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
|
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
|
||||||
|
@ -217,7 +217,11 @@ public abstract class SupportManager {
|
||||||
synchronized (dispute.getChatMessages()) {
|
synchronized (dispute.getChatMessages()) {
|
||||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||||
if (trade.getDisputeState().isCloseRequested()) {
|
if (trade.getDisputeState().isRequested()) {
|
||||||
|
log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||||
|
dispute.setIsClosed();
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
} else if (trade.getDisputeState().isCloseRequested()) {
|
||||||
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||||
dispute.setIsClosed();
|
dispute.setIsClosed();
|
||||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
|
|
@ -392,7 +392,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
change += "chat messages;";
|
change += "chat messages;";
|
||||||
}
|
}
|
||||||
if (change.length() > 0) {
|
if (change.length() > 0) {
|
||||||
log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId()));
|
log.info("Cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue