mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-04-23 08:59:24 -04:00
Compare commits
270 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
de5250e89a | ||
![]() |
923b3ad73b | ||
![]() |
9a14d5552e | ||
![]() |
a3d3f51f02 | ||
![]() |
77429472f4 | ||
![]() |
38615edf86 | ||
![]() |
cf9a37f295 | ||
![]() |
c7a3a9740f | ||
![]() |
13e13d945d | ||
![]() |
bfef0f9492 | ||
![]() |
39909e7936 | ||
![]() |
695f2b8dd3 | ||
![]() |
8f778be4d9 | ||
![]() |
821ef16d8f | ||
![]() |
58590d60df | ||
![]() |
8eccbcce43 | ||
![]() |
bbfc5d5fed | ||
![]() |
22db354cb2 | ||
![]() |
60ceff6695 | ||
![]() |
c87b8a5b45 | ||
![]() |
bf055556f1 | ||
![]() |
13dc34a805 | ||
![]() |
53b0f203de | ||
![]() |
52f0c20c8c | ||
![]() |
a30b41de4b | ||
![]() |
f1c09161f4 | ||
![]() |
57c2408d07 | ||
![]() |
f5515caad5 | ||
![]() |
71b23e0ed9 | ||
![]() |
9a5d2d5862 | ||
![]() |
31782e5255 | ||
![]() |
96eab3d42f | ||
![]() |
454fc91298 | ||
![]() |
5bff265cca | ||
![]() |
295c91760c | ||
![]() |
f19ed19325 | ||
![]() |
31fbf9c4e8 | ||
![]() |
765a32fd9f | ||
![]() |
fe1fb88ce0 | ||
![]() |
35eb65d173 | ||
![]() |
974c6a0d86 | ||
![]() |
ad38e3b80c | ||
![]() |
7243d7fa38 | ||
![]() |
d78709e1f9 | ||
![]() |
34b55bc86b | ||
![]() |
501485ec71 | ||
![]() |
1c92d96651 | ||
![]() |
08b0b36436 | ||
![]() |
9027ce6634 | ||
![]() |
3c6914ac7e | ||
![]() |
055b7d1376 | ||
![]() |
f87dc3a4d1 | ||
![]() |
40e18890d6 | ||
![]() |
7e3a47de4a | ||
![]() |
9668dd2369 | ||
![]() |
9bd4f70d02 | ||
![]() |
39c75cd71b | ||
![]() |
8981740b8c | ||
![]() |
584cc3b6d4 | ||
![]() |
dc43e1c329 | ||
![]() |
93369c4211 | ||
![]() |
a21971429c | ||
![]() |
df9daf99bf | ||
![]() |
e699b427e2 | ||
![]() |
29e6540234 | ||
![]() |
207ff5416c | ||
![]() |
ad809ff20e | ||
![]() |
b3174518d9 | ||
![]() |
82728aef69 | ||
![]() |
1a2dcfc704 | ||
![]() |
ce27818f43 | ||
![]() |
a7d8e4560f | ||
![]() |
028ced7021 | ||
![]() |
c95a26e043 | ||
![]() |
b5f9bc307b | ||
![]() |
bee86daff3 | ||
![]() |
26e3a153bc | ||
![]() |
2af9019db0 | ||
![]() |
5107c6ba57 | ||
![]() |
79aa214f22 | ||
![]() |
6f3ae49b68 | ||
![]() |
5711aabad8 | ||
![]() |
d7be2885bd | ||
![]() |
51fc4d0c41 | ||
![]() |
1b31dc24b8 | ||
![]() |
b19724e33d | ||
![]() |
07fa0b35e4 | ||
![]() |
f4f53630d5 | ||
![]() |
cb25a23779 | ||
![]() |
b7c9dea518 | ||
![]() |
34458cf3df | ||
![]() |
63917fe8cc | ||
![]() |
d4eb30bb97 | ||
![]() |
cb69d06468 | ||
![]() |
46734459d4 | ||
![]() |
a55daf803e | ||
![]() |
1510e6f18d | ||
![]() |
fb2b4a0c6a | ||
![]() |
38c0855728 | ||
![]() |
84d8a17ab4 | ||
![]() |
00a2a7c2b7 | ||
![]() |
251a973fd6 | ||
![]() |
bedd38748e | ||
![]() |
b0e9627c10 | ||
![]() |
bf97fbc7ea | ||
![]() |
8b1d2aa203 | ||
![]() |
2d46b2ab7c | ||
![]() |
9acd7ad584 | ||
![]() |
c853c4ffcb | ||
![]() |
e5f729d12f | ||
![]() |
03a1132c2f | ||
![]() |
61a62a1d94 | ||
![]() |
a53026be8a | ||
![]() |
6b567b94f2 | ||
![]() |
c9350e123e | ||
![]() |
68f7067125 | ||
![]() |
d67d259b2c | ||
![]() |
e24b1c2461 | ||
![]() |
67d0589e7b | ||
![]() |
060d9fa4f1 | ||
![]() |
52bf1edf79 | ||
![]() |
580e5b672c | ||
![]() |
5720ee74b0 | ||
![]() |
fff0fa0186 | ||
![]() |
31b0edca22 | ||
![]() |
48501a6572 | ||
![]() |
998b893cc3 | ||
![]() |
816d273956 | ||
![]() |
28d2bc891f | ||
![]() |
964c71ed1b | ||
![]() |
40924a6f7b | ||
![]() |
8a01a07ac2 | ||
![]() |
5d457d62c5 | ||
![]() |
b9381f7f9f | ||
![]() |
4d765fa5d9 | ||
![]() |
e4fa5f520d | ||
![]() |
c3b7289943 | ||
![]() |
024e59a982 | ||
![]() |
667f0c8fb5 | ||
![]() |
0cba254193 | ||
![]() |
f675588a2d | ||
![]() |
290a3738b7 | ||
![]() |
4a82c69507 | ||
![]() |
b72159fcf8 | ||
![]() |
cd71bcdde7 | ||
![]() |
bd3fffada4 | ||
![]() |
bffcf7c7c0 | ||
![]() |
c26974610c | ||
![]() |
5f8cf97d16 | ||
![]() |
afa95f1b15 | ||
![]() |
728cf22578 | ||
![]() |
f35c7f8544 | ||
![]() |
b48dbc2fb3 | ||
![]() |
8038dcf401 | ||
![]() |
ae8760d72c | ||
![]() |
71fab722ee | ||
![]() |
c333803917 | ||
![]() |
e6b29b88f5 | ||
![]() |
352384b41e | ||
![]() |
12def5f1b5 | ||
![]() |
88c3f04be0 | ||
![]() |
97569bad37 | ||
![]() |
1d4dbe7ce0 | ||
![]() |
a014740014 | ||
![]() |
9b50cd7ba7 | ||
![]() |
6c6c6e2dd5 | ||
![]() |
a6af1550a4 | ||
![]() |
e4714aab89 | ||
![]() |
0d2c1fe8fd | ||
![]() |
dc7a8e4201 | ||
![]() |
c3f7f194b0 | ||
![]() |
3847d1bd3a | ||
![]() |
535b71adc5 | ||
![]() |
66770cc98f | ||
![]() |
39bc54df73 | ||
![]() |
7bc341d69f | ||
![]() |
9a74856fa2 | ||
![]() |
a8fb638594 | ||
![]() |
2d4455b1a2 | ||
![]() |
bf8f4cea73 | ||
![]() |
fac901331f | ||
![]() |
130a45c99a | ||
![]() |
b571b39790 | ||
![]() |
88b6bed93e | ||
![]() |
97475d84e9 | ||
![]() |
69da858365 | ||
![]() |
e1b3cdce28 | ||
![]() |
7fba0faac1 | ||
![]() |
5e6bf9e22b | ||
![]() |
0f5f7ae46e | ||
![]() |
6301bde10e | ||
![]() |
2f322674f8 | ||
![]() |
533527e362 | ||
![]() |
b0c1dceb56 | ||
![]() |
d9f9c1e736 | ||
![]() |
1ac4c45f6d | ||
![]() |
e426f4d8f1 | ||
![]() |
944c189166 | ||
![]() |
e8d5366941 | ||
![]() |
3e0b694e13 | ||
![]() |
21ea08a68d | ||
![]() |
a1a7f9ccc9 | ||
![]() |
e4f3d13660 | ||
![]() |
25f85f9f8d | ||
![]() |
a9325356c4 | ||
![]() |
9e95de2d7e | ||
![]() |
0462ddc273 | ||
![]() |
c1b17cf612 | ||
![]() |
89007c496e | ||
![]() |
2dc7405f82 | ||
![]() |
6a798312fe | ||
![]() |
fc1388d2f4 | ||
![]() |
cccd9cf094 | ||
![]() |
018ac61054 | ||
![]() |
ed87b36a76 | ||
![]() |
adcf158a90 | ||
![]() |
f053a274a4 | ||
![]() |
fdee044023 | ||
![]() |
42ede83ca2 | ||
![]() |
5444d96832 | ||
![]() |
7340ca9c21 | ||
![]() |
542441d9d2 | ||
![]() |
c5ef60ce5c | ||
![]() |
389c5dddac | ||
![]() |
7240b5f222 | ||
![]() |
34e0c4b71f | ||
![]() |
aab4d0207e | ||
![]() |
1a51b171a0 | ||
![]() |
a557d90e5d | ||
![]() |
7e4e950710 | ||
![]() |
323d14feb0 | ||
![]() |
5c79380e63 | ||
![]() |
af3c7059a9 | ||
![]() |
7e29dc188d | ||
![]() |
544d69827a | ||
![]() |
c75e3aa455 | ||
![]() |
bd5accb5a5 | ||
![]() |
775fbc41c2 | ||
![]() |
ece3b0fec0 | ||
![]() |
4b7db9a1ae | ||
![]() |
140961d885 | ||
![]() |
9ec2794931 | ||
![]() |
85acb8aeb3 | ||
![]() |
19398bb73e | ||
![]() |
0275de3ff6 | ||
![]() |
b586bc57f6 | ||
![]() |
7f6d28f1fb | ||
![]() |
1aef8a6bab | ||
![]() |
71987400c7 | ||
![]() |
e05ab6f7ed | ||
![]() |
cfaf163bbc | ||
![]() |
dc8d854709 | ||
![]() |
1f385328de | ||
![]() |
98e2df3c7e | ||
![]() |
103c45d412 | ||
![]() |
c9cf5351c0 | ||
![]() |
bf452c91da | ||
![]() |
a5417994d6 | ||
![]() |
c40e0bea5a | ||
![]() |
ae80935f3a | ||
![]() |
68b4a0fafb | ||
![]() |
8fd7f17317 | ||
![]() |
24657c6c57 | ||
![]() |
ba763f7bf6 | ||
![]() |
264cb5f0ac | ||
![]() |
c9e992442c | ||
![]() |
86e67d384c | ||
![]() |
59d8a8ee44 | ||
![]() |
5221782ba0 | ||
![]() |
023e2bcd2f |
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@ -1,3 +1,6 @@
|
|||||||
|
# GitHub Releases requires a tag, e.g:
|
||||||
|
# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1"
|
||||||
|
# git push origin 1.0.19-1
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@ -26,21 +29,23 @@ jobs:
|
|||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --stacktrace --scan
|
run: ./gradlew build --stacktrace --scan
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: error-reports-${{ matrix.os }}
|
name: error-reports-${{ matrix.os }}
|
||||||
path: ${{ github.workspace }}/desktop/build/reports
|
path: ${{ github.workspace }}/desktop/build/reports
|
||||||
- name: cache nodes dependencies
|
- name: cache nodes dependencies
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
include-hidden-files: true
|
||||||
name: cached-localnet
|
name: cached-localnet
|
||||||
path: .localnet
|
path: .localnet
|
||||||
|
overwrite: true
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt-get update
|
||||||
sudo apt 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: ${{ matrix.os == 'windows-latest' }}
|
||||||
@ -67,10 +72,9 @@ jobs:
|
|||||||
"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 on Unix
|
- name: Move Release Files for Linux
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then
|
|
||||||
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
|
||||||
@ -83,58 +87,87 @@ jobs:
|
|||||||
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
|
||||||
else
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||||
|
shell: bash
|
||||||
|
- name: Move Release Files for macOS
|
||||||
|
if: ${{ matrix.os == 'macos-13' }}
|
||||||
|
run: |
|
||||||
mkdir ${{ github.workspace }}/release-macos
|
mkdir ${{ github.workspace }}/release-macos
|
||||||
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/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
||||||
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
|
||||||
fi
|
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-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: ${{ matrix.os == 'windows-latest' }}
|
||||||
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-installer.exe
|
||||||
Move-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
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|
||||||
# win
|
# win
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Windows artifacts"
|
name: "Windows artifacts"
|
||||||
if: ${{ matrix.os == 'windows-latest'}}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-windows
|
name: haveno-windows
|
||||||
path: ${{ github.workspace }}/release-windows
|
path: ${{ github.workspace }}/release-windows
|
||||||
# macos
|
# macos
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: "macOS artifacts"
|
name: "macOS artifacts"
|
||||||
if: ${{ matrix.os == 'macos-13' }}
|
if: ${{ matrix.os == 'macos-13' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-macos
|
name: haveno-macos
|
||||||
path: ${{ github.workspace }}/release-macos
|
path: ${{ github.workspace }}/release-macos
|
||||||
# linux
|
# linux
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - deb artifact"
|
name: "Linux - deb artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-deb
|
name: haveno-linux-deb
|
||||||
path: ${{ github.workspace }}/release-linux-deb
|
path: ${{ github.workspace }}/release-linux-deb
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - rpm artifact"
|
name: "Linux - rpm artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-rpm
|
name: haveno-linux-rpm
|
||||||
path: ${{ github.workspace }}/release-linux-rpm
|
path: ${{ github.workspace }}/release-linux-rpm
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - AppImage artifact"
|
name: "Linux - AppImage artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-appimage
|
name: haveno-linux-appimage
|
||||||
path: ${{ github.workspace }}/release-linux-appimage
|
path: ${{ github.workspace }}/release-linux-appimage
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
name: "Linux - flatpak artifact"
|
name: "Linux - flatpak artifact"
|
||||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||||
with:
|
with:
|
||||||
name: haveno-linux-flatpak
|
name: haveno-linux-flatpak
|
||||||
path: ${{ github.workspace }}/release-linux-flatpak
|
path: ${{ github.workspace }}/release-linux-flatpak
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
|
||||||
|
${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
|
||||||
|
${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
|
||||||
|
${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
|
||||||
|
${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
|
||||||
|
${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
|
||||||
|
${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256
|
||||||
|
|
||||||
|
# https://git-scm.com/docs/git-tag - git-tag Docu
|
||||||
|
#
|
||||||
|
# git tag - lists all local tags
|
||||||
|
# git tag -d 1.0.19-1 - delete local tag
|
||||||
|
#
|
||||||
|
# git ls-remote --tags - lists all remote tags
|
||||||
|
# git push origin --delete refs/tags/1.0.19-1 - delete remote tag
|
||||||
|
2
.github/workflows/codacy-code-reporter.yml
vendored
2
.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-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -18,7 +18,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -68,4 +68,4 @@ jobs:
|
|||||||
run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest
|
run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
2
.github/workflows/label.yml
vendored
2
.github/workflows/label.yml
vendored
@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
issueLabeled:
|
issueLabeled:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.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
|
||||||
|
6
LICENSE
6
LICENSE
@ -1,7 +1,7 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Copyright (C) 2020 Haveno Dex
|
Copyright (C) 2020 Haveno Dex
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
GNU Affero General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
@ -659,4 +659,4 @@ specific requirements.
|
|||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
42
Makefile
42
Makefile
@ -70,9 +70,11 @@ 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 \
|
||||||
|
|
||||||
monerod2-local:
|
monerod2-local:
|
||||||
./.localnet/monerod \
|
./.localnet/monerod \
|
||||||
@ -88,9 +90,11 @@ 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 \
|
||||||
|
|
||||||
monerod3-local:
|
monerod3-local:
|
||||||
./.localnet/monerod \
|
./.localnet/monerod \
|
||||||
@ -106,9 +110,11 @@ 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 \
|
||||||
|
|
||||||
#--proxy 127.0.0.1:49775 \
|
#--proxy 127.0.0.1:49775 \
|
||||||
|
|
||||||
@ -417,6 +423,17 @@ haveno-desktop-stagenet:
|
|||||||
--apiPort=3204 \
|
--apiPort=3204 \
|
||||||
--useNativeXmrWallet=false \
|
--useNativeXmrWallet=false \
|
||||||
|
|
||||||
|
haveno-daemon-stagenet:
|
||||||
|
./haveno-daemon$(APP_EXT) \
|
||||||
|
--baseCurrencyNetwork=XMR_STAGENET \
|
||||||
|
--useLocalhostForP2P=false \
|
||||||
|
--useDevPrivilegeKeys=false \
|
||||||
|
--nodePort=9999 \
|
||||||
|
--appName=Haveno \
|
||||||
|
--apiPassword=apitest \
|
||||||
|
--apiPort=3204 \
|
||||||
|
--useNativeXmrWallet=false \
|
||||||
|
|
||||||
# Mainnet network
|
# Mainnet network
|
||||||
|
|
||||||
monerod:
|
monerod:
|
||||||
@ -468,6 +485,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 \
|
||||||
|
26
README.md
26
README.md
@ -23,13 +23,22 @@ Main features:
|
|||||||
|
|
||||||
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
|
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
|
||||||
|
|
||||||
|
## Haveno Demo
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47
|
||||||
|
|
||||||
## Installing Haveno
|
## Installing Haveno
|
||||||
|
|
||||||
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. We do not endorse any networks at this time.
|
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network.
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> The official Haveno repository does not support making real trades directly.
|
||||||
|
>
|
||||||
|
> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time.
|
||||||
|
|
||||||
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network.
|
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network.
|
||||||
|
|
||||||
Alternatively, you can [start your own network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md).
|
Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md).
|
||||||
|
|
||||||
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
|
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
|
||||||
|
|
||||||
@ -47,7 +56,7 @@ If you wish to help, take a look at the repositories above and look for open iss
|
|||||||
Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server:
|
Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server:
|
||||||
|
|
||||||
- General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`)
|
- General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`)
|
||||||
- Development discussions: **Haveno Development** ([#haveno-dev:monero.social](https://matrix.to/#/#haveno-dev:monero.social)) relayed on IRC/Libera (`#haveno-dev`)
|
- Development discussions: **Haveno Development** ([#haveno-development:monero.social](https://matrix.to/#/#haveno-development:monero.social)) relayed on IRC/Libera (`#haveno-development`)
|
||||||
|
|
||||||
Email: contact@haveno.exchange
|
Email: contact@haveno.exchange
|
||||||
Website: [haveno.exchange](https://haveno.exchange)
|
Website: [haveno.exchange](https://haveno.exchange)
|
||||||
@ -58,7 +67,7 @@ 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) fund development bounties.
|
If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties.
|
||||||
|
|
||||||
## Bounties
|
## Bounties
|
||||||
|
|
||||||
@ -68,18 +77,9 @@ To incentivize development and reward contributors, we adopt a simple bounty sys
|
|||||||
|
|
||||||
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 [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project:
|
||||||
|
|
||||||
### Monero
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<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>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</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.
|
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
|
||||||
|
|
||||||
### Bitcoin
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="115" height="115"><br>
|
|
||||||
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
|
|
||||||
</p>
|
|
||||||
|
@ -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.getDefaultBuyerSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
|
||||||
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;
|
||||||
@ -157,8 +157,8 @@ public class MethodTest extends ApiTestCase {
|
|||||||
return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
|
return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Supplier<Double> defaultBuyerSecurityDepositPct = () -> {
|
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
|
||||||
var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent());
|
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
|
||||||
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."
|
||||||
|
@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
|
|||||||
10000000L,
|
10000000L,
|
||||||
10000000L,
|
10000000L,
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
paymentAccountId,
|
paymentAccountId,
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
};
|
};
|
||||||
|
@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
10_000_000L,
|
10_000_000L,
|
||||||
"36000",
|
"36000",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
audAccount.getId());
|
audAccount.getId());
|
||||||
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
10_000_000L,
|
10_000_000L,
|
||||||
"30000.1234",
|
"30000.1234",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
usdAccount.getId());
|
usdAccount.getId());
|
||||||
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
5_000_000L,
|
5_000_000L,
|
||||||
"29500.1234",
|
"29500.1234",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
eurAccount.getId());
|
eurAccount.getId());
|
||||||
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
|
@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
10_000_000L,
|
10_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
usdAccount.getId(),
|
usdAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
|
||||||
@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
10_000_000L,
|
10_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
nzdAccount.getId(),
|
nzdAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
|
||||||
@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
5_000_000L,
|
5_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
gbpAccount.getId(),
|
gbpAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
|
||||||
@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
5_000_000L,
|
5_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
brlAccount.getId(),
|
brlAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
|
||||||
@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
|||||||
10_000_000L,
|
10_000_000L,
|
||||||
5_000_000L,
|
5_000_000L,
|
||||||
0.0,
|
0.0,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
usdAccount.getId(),
|
usdAccount.getId(),
|
||||||
triggerPrice);
|
triggerPrice);
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
|
@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
|||||||
100_000_000L,
|
100_000_000L,
|
||||||
75_000_000L,
|
75_000_000L,
|
||||||
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
|
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId());
|
alicesXmrAcct.getId());
|
||||||
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
|||||||
100_000_000L,
|
100_000_000L,
|
||||||
50_000_000L,
|
50_000_000L,
|
||||||
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId());
|
alicesXmrAcct.getId());
|
||||||
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||||
assertTrue(newOffer.getIsMyOffer());
|
assertTrue(newOffer.getIsMyOffer());
|
||||||
@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
|||||||
100_000_000L,
|
100_000_000L,
|
||||||
75_000_000L,
|
75_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId(),
|
alicesXmrAcct.getId(),
|
||||||
triggerPrice);
|
triggerPrice);
|
||||||
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||||
@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
|
|||||||
100_000_000L,
|
100_000_000L,
|
||||||
50_000_000L,
|
50_000_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId(),
|
alicesXmrAcct.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
|
||||||
|
@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
|||||||
100000000000L, // exceeds amount limit
|
100000000000L, // exceeds amount limit
|
||||||
100000000000L,
|
100000000000L,
|
||||||
"10000.0000",
|
"10000.0000",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
usdAccount.getId()));
|
usdAccount.getId()));
|
||||||
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
|
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
|||||||
10000000L,
|
10000000L,
|
||||||
10000000L,
|
10000000L,
|
||||||
"40000.0000",
|
"40000.0000",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
chfAccount.getId()));
|
chfAccount.getId()));
|
||||||
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
|
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
|
||||||
assertEquals(expectedError, exception.getMessage());
|
assertEquals(expectedError, exception.getMessage());
|
||||||
@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
|
|||||||
10000000L,
|
10000000L,
|
||||||
10000000L,
|
10000000L,
|
||||||
"63000.0000",
|
"63000.0000",
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
audAccount.getId()));
|
audAccount.getId()));
|
||||||
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
|
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
|
||||||
assertEquals(expectedError, exception.getMessage());
|
assertEquals(expectedError, exception.getMessage());
|
||||||
|
@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
|
|||||||
12_500_000L,
|
12_500_000L,
|
||||||
12_500_000L, // min-amount = amount
|
12_500_000L, // min-amount = amount
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesUsdAccount.getId(),
|
alicesUsdAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
var offerId = alicesOffer.getId();
|
var offerId = alicesOffer.getId();
|
||||||
|
@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
|
|||||||
1_000_000L,
|
1_000_000L,
|
||||||
1_000_000L, // min-amount = amount
|
1_000_000L, // min-amount = amount
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesPaymentAccount.getId(),
|
alicesPaymentAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
var offerId = alicesOffer.getId();
|
var offerId = alicesOffer.getId();
|
||||||
|
@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
|
|||||||
15_000_000L,
|
15_000_000L,
|
||||||
7_500_000L,
|
7_500_000L,
|
||||||
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId());
|
alicesXmrAcct.getId());
|
||||||
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
||||||
genBtcBlocksThenWait(1, 5000);
|
genBtcBlocksThenWait(1, 5000);
|
||||||
|
@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
|||||||
12_500_000L,
|
12_500_000L,
|
||||||
12_500_000L, // min-amount = amount
|
12_500_000L, // min-amount = amount
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesUsdAccount.getId(),
|
alicesUsdAccount.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
var offerId = alicesOffer.getId();
|
var offerId = alicesOffer.getId();
|
||||||
|
@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
|
|||||||
20_000_000L,
|
20_000_000L,
|
||||||
10_500_000L,
|
10_500_000L,
|
||||||
priceMarginPctInput,
|
priceMarginPctInput,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
alicesXmrAcct.getId(),
|
alicesXmrAcct.getId(),
|
||||||
NO_TRIGGER_PRICE);
|
NO_TRIGGER_PRICE);
|
||||||
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
|
||||||
|
@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
|
|||||||
1_000_000,
|
1_000_000,
|
||||||
1_000_000,
|
1_000_000,
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
paymentAcct.getId(),
|
paymentAcct.getId(),
|
||||||
triggerPrice);
|
triggerPrice);
|
||||||
log.info("SELL offer {} created with margin based price {}.",
|
log.info("SELL offer {} created with margin based price {}.",
|
||||||
@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
|
|||||||
1_000_000,
|
1_000_000,
|
||||||
1_000_000,
|
1_000_000,
|
||||||
0.00,
|
0.00,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
paymentAcct.getId(),
|
paymentAcct.getId(),
|
||||||
triggerPrice);
|
triggerPrice);
|
||||||
log.info("BUY offer {} created with margin based price {}.",
|
log.info("BUY offer {} created with margin based price {}.",
|
||||||
|
@ -28,7 +28,7 @@ import java.text.DecimalFormat;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
|
import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct;
|
||||||
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
|
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
|
||||||
import static haveno.cli.CurrencyFormat.formatSatoshis;
|
import static haveno.cli.CurrencyFormat.formatSatoshis;
|
||||||
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
|
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
|
||||||
@ -119,7 +119,7 @@ public class RandomOffer {
|
|||||||
amount,
|
amount,
|
||||||
minAmount,
|
minAmount,
|
||||||
priceMargin,
|
priceMargin,
|
||||||
defaultBuyerSecurityDepositPct.get(),
|
defaultSecurityDepositPct.get(),
|
||||||
"0" /*no trigger price*/);
|
"0" /*no trigger price*/);
|
||||||
} else {
|
} else {
|
||||||
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
|
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
|
||||||
@ -128,7 +128,7 @@ public class RandomOffer {
|
|||||||
amount,
|
amount,
|
||||||
minAmount,
|
minAmount,
|
||||||
fixedOfferPrice,
|
fixedOfferPrice,
|
||||||
defaultBuyerSecurityDepositPct.get());
|
defaultSecurityDepositPct.get());
|
||||||
}
|
}
|
||||||
this.id = offer.getId();
|
this.id = offer.getId();
|
||||||
return this;
|
return this;
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
|
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
|
||||||
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
|
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
|
||||||
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
|
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
|
||||||
* {@link haveno.asset.tokens.DaiStablecoin}.
|
* {@link haveno.asset.tokens.DaiStablecoinERC20}.
|
||||||
* <p>
|
* <p>
|
||||||
* The purpose of this package is to provide everything necessary for registering
|
* The purpose of this package is to provide everything necessary for registering
|
||||||
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno
|
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno
|
||||||
|
@ -19,9 +19,9 @@ package haveno.asset.tokens;
|
|||||||
|
|
||||||
import haveno.asset.Erc20Token;
|
import haveno.asset.Erc20Token;
|
||||||
|
|
||||||
public class USDCoin extends Erc20Token {
|
public class DaiStablecoinERC20 extends Erc20Token {
|
||||||
|
|
||||||
public USDCoin() {
|
public DaiStablecoinERC20() {
|
||||||
super("USD Coin", "USDC");
|
super("Dai Stablecoin", "DAI-ERC20");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,9 +19,9 @@ package haveno.asset.tokens;
|
|||||||
|
|
||||||
import haveno.asset.Erc20Token;
|
import haveno.asset.Erc20Token;
|
||||||
|
|
||||||
public class DaiStablecoin extends Erc20Token {
|
public class USDCoinERC20 extends Erc20Token {
|
||||||
|
|
||||||
public DaiStablecoin() {
|
public USDCoinERC20() {
|
||||||
super("Dai Stablecoin", "DAI");
|
super("USD Coin (ERC20)", "USDC-ERC20");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,3 +9,5 @@ haveno.asset.coins.Litecoin
|
|||||||
haveno.asset.coins.Monero
|
haveno.asset.coins.Monero
|
||||||
haveno.asset.tokens.TetherUSDERC20
|
haveno.asset.tokens.TetherUSDERC20
|
||||||
haveno.asset.tokens.TetherUSDTRC20
|
haveno.asset.tokens.TetherUSDTRC20
|
||||||
|
haveno.asset.tokens.USDCoinERC20
|
||||||
|
haveno.asset.tokens.DaiStablecoinERC20
|
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.33'
|
moneroJavaVersion = '0.8.36'
|
||||||
httpclient5Version = '5.0'
|
httpclient5Version = '5.0'
|
||||||
hamcrestVersion = '2.2'
|
hamcrestVersion = '2.2'
|
||||||
httpclientVersion = '4.5.12'
|
httpclientVersion = '4.5.12'
|
||||||
@ -71,7 +71,7 @@ configure(subprojects) {
|
|||||||
loggingVersion = '1.2'
|
loggingVersion = '1.2'
|
||||||
lombokVersion = '1.18.30'
|
lombokVersion = '1.18.30'
|
||||||
mockitoVersion = '5.10.0'
|
mockitoVersion = '5.10.0'
|
||||||
netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11
|
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
|
||||||
protobufVersion = '3.19.1'
|
protobufVersion = '3.19.1'
|
||||||
protocVersion = protobufVersion
|
protocVersion = protobufVersion
|
||||||
pushyVersion = '0.13.2'
|
pushyVersion = '0.13.2'
|
||||||
@ -457,14 +457,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/release4/monero-bins-haveno-linux-x86_64.tar.gz',
|
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz',
|
||||||
'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68',
|
'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
|
||||||
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz',
|
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
|
||||||
'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de',
|
'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
|
||||||
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz',
|
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
|
||||||
'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287',
|
'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
|
||||||
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip',
|
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
|
||||||
'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888'
|
'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
|
||||||
]
|
]
|
||||||
|
|
||||||
String osKey
|
String osKey
|
||||||
@ -506,6 +506,7 @@ configure(project(':core')) {
|
|||||||
} else {
|
} else {
|
||||||
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
|
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// add the current platform's monero dependencies into the resources folder for installation
|
// add the current platform's monero dependencies into the resources folder for installation
|
||||||
copy {
|
copy {
|
||||||
@ -517,7 +518,6 @@ configure(project(':core')) {
|
|||||||
into "${project(':core').projectDir}/src/main/resources/bin"
|
into "${project(':core').projectDir}/src/main/resources/bin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ext.extractArchiveTarGz = { File tarGzFile, File destinationDir ->
|
ext.extractArchiveTarGz = { File tarGzFile, File destinationDir ->
|
||||||
println "Extracting tar.gz ${tarGzFile}"
|
println "Extracting tar.gz ${tarGzFile}"
|
||||||
@ -610,7 +610,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.13-SNAPSHOT'
|
version = '1.1.0-SNAPSHOT'
|
||||||
|
|
||||||
jar.manifest.attributes(
|
jar.manifest.attributes(
|
||||||
"Implementation-Title": project.name,
|
"Implementation-Title": project.name,
|
||||||
|
@ -81,7 +81,7 @@ public class OffersServiceRequest {
|
|||||||
.setUseMarketBasedPrice(useMarketBasedPrice)
|
.setUseMarketBasedPrice(useMarketBasedPrice)
|
||||||
.setPrice(fixedPrice)
|
.setPrice(fixedPrice)
|
||||||
.setMarketPriceMarginPct(marketPriceMarginPct)
|
.setMarketPriceMarginPct(marketPriceMarginPct)
|
||||||
.setBuyerSecurityDepositPct(securityDepositPct)
|
.setSecurityDepositPct(securityDepositPct)
|
||||||
.setPaymentAccountId(paymentAcctId)
|
.setPaymentAccountId(paymentAcctId)
|
||||||
.setTriggerPrice(triggerPrice)
|
.setTriggerPrice(triggerPrice)
|
||||||
.build();
|
.build();
|
||||||
|
@ -69,7 +69,7 @@ public class ClockWatcher {
|
|||||||
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
|
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
|
||||||
|
|
||||||
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
|
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
|
||||||
log.info("We have been in standby mode for {} sec", missedMs / 1000);
|
log.warn("We have been in standby mode for {} sec", missedMs / 1000);
|
||||||
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
|
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,10 +59,12 @@ public class Capabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Capabilities(Collection<Capability> capabilities) {
|
public Capabilities(Collection<Capability> capabilities) {
|
||||||
|
synchronized (capabilities) {
|
||||||
synchronized (this.capabilities) {
|
synchronized (this.capabilities) {
|
||||||
this.capabilities.addAll(capabilities);
|
this.capabilities.addAll(capabilities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void set(Capability... capabilities) {
|
public void set(Capability... capabilities) {
|
||||||
set(Arrays.asList(capabilities));
|
set(Arrays.asList(capabilities));
|
||||||
@ -73,11 +75,13 @@ public class Capabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void set(Collection<Capability> capabilities) {
|
public void set(Collection<Capability> capabilities) {
|
||||||
|
synchronized (capabilities) {
|
||||||
synchronized (this.capabilities) {
|
synchronized (this.capabilities) {
|
||||||
this.capabilities.clear();
|
this.capabilities.clear();
|
||||||
this.capabilities.addAll(capabilities);
|
this.capabilities.addAll(capabilities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void addAll(Capability... capabilities) {
|
public void addAll(Capability... capabilities) {
|
||||||
synchronized (this.capabilities) {
|
synchronized (this.capabilities) {
|
||||||
@ -87,17 +91,21 @@ public class Capabilities {
|
|||||||
|
|
||||||
public void addAll(Capabilities capabilities) {
|
public void addAll(Capabilities capabilities) {
|
||||||
if (capabilities != null) {
|
if (capabilities != null) {
|
||||||
|
synchronized (capabilities.capabilities) {
|
||||||
synchronized (this.capabilities) {
|
synchronized (this.capabilities) {
|
||||||
this.capabilities.addAll(capabilities.capabilities);
|
this.capabilities.addAll(capabilities.capabilities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsAll(final Set<Capability> requiredItems) {
|
public boolean containsAll(final Set<Capability> requiredItems) {
|
||||||
|
synchronized(requiredItems) {
|
||||||
synchronized (this.capabilities) {
|
synchronized (this.capabilities) {
|
||||||
return capabilities.containsAll(requiredItems);
|
return capabilities.containsAll(requiredItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsAll(final Capabilities capabilities) {
|
public boolean containsAll(final Capabilities capabilities) {
|
||||||
return containsAll(capabilities.capabilities);
|
return containsAll(capabilities.capabilities);
|
||||||
@ -129,8 +137,10 @@ public class Capabilities {
|
|||||||
* @return int list of Capability ordinals
|
* @return int list of Capability ordinals
|
||||||
*/
|
*/
|
||||||
public static List<Integer> toIntList(Capabilities capabilities) {
|
public static List<Integer> toIntList(Capabilities capabilities) {
|
||||||
|
synchronized (capabilities.capabilities) {
|
||||||
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
|
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper for protobuffer stuff
|
* helper for protobuffer stuff
|
||||||
@ -139,12 +149,14 @@ public class Capabilities {
|
|||||||
* @return a {@link Capabilities} object
|
* @return a {@link Capabilities} object
|
||||||
*/
|
*/
|
||||||
public static Capabilities fromIntList(List<Integer> capabilities) {
|
public static Capabilities fromIntList(List<Integer> capabilities) {
|
||||||
|
synchronized (capabilities) {
|
||||||
return new Capabilities(capabilities.stream()
|
return new Capabilities(capabilities.stream()
|
||||||
.filter(integer -> integer < Capability.values().length)
|
.filter(integer -> integer < Capability.values().length)
|
||||||
.filter(integer -> integer >= 0)
|
.filter(integer -> integer >= 0)
|
||||||
.map(integer -> Capability.values()[integer])
|
.map(integer -> Capability.values()[integer])
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -181,8 +193,10 @@ public class Capabilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
|
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
|
||||||
|
synchronized (capabilities.capabilities) {
|
||||||
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
|
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
@ -211,8 +225,10 @@ public class Capabilities {
|
|||||||
// Neither would support removal of past capabilities, a use case we never had so far and which might have
|
// Neither would support removal of past capabilities, a use case we never had so far and which might have
|
||||||
// backward compatibility issues, so we should treat capabilities as an append-only data structure.
|
// backward compatibility issues, so we should treat capabilities as an append-only data structure.
|
||||||
public int findHighestCapability(Capabilities capabilities) {
|
public int findHighestCapability(Capabilities capabilities) {
|
||||||
|
synchronized (capabilities.capabilities) {
|
||||||
return (int) capabilities.capabilities.stream()
|
return (int) capabilities.capabilities.stream()
|
||||||
.mapToLong(e -> (long) e.ordinal())
|
.mapToLong(e -> (long) e.ordinal())
|
||||||
.sum();
|
.sum();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.13";
|
public static final String VERSION = "1.1.0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -72,6 +72,25 @@ public class Version {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int compare(String version1, String version2) {
|
||||||
|
if (version1.equals(version2))
|
||||||
|
return 0;
|
||||||
|
else if (getMajorVersion(version1) > getMajorVersion(version2))
|
||||||
|
return 1;
|
||||||
|
else if (getMajorVersion(version1) < getMajorVersion(version2))
|
||||||
|
return -1;
|
||||||
|
else if (getMinorVersion(version1) > getMinorVersion(version2))
|
||||||
|
return 1;
|
||||||
|
else if (getMinorVersion(version1) < getMinorVersion(version2))
|
||||||
|
return -1;
|
||||||
|
else if (getPatchVersion(version1) > getPatchVersion(version2))
|
||||||
|
return 1;
|
||||||
|
else if (getPatchVersion(version1) < getPatchVersion(version2))
|
||||||
|
return -1;
|
||||||
|
else
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static int getSubVersion(String version, int index) {
|
private static int getSubVersion(String version, int index) {
|
||||||
final String[] split = version.split("\\.");
|
final String[] split = version.split("\\.");
|
||||||
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
|
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
|
||||||
@ -91,8 +110,9 @@ public class Version {
|
|||||||
// 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 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
|
// 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
|
||||||
public static final int TRADE_PROTOCOL_VERSION = 1;
|
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
|
||||||
|
public static final int TRADE_PROTOCOL_VERSION = 2;
|
||||||
private static String p2pMessageVersion;
|
private static String p2pMessageVersion;
|
||||||
|
|
||||||
public static String getP2PMessageVersion() {
|
public static String getP2PMessageVersion() {
|
||||||
|
@ -117,6 +117,8 @@ public class Config {
|
|||||||
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
|
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
|
||||||
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
||||||
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 XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
|
||||||
|
|
||||||
// Default values for certain options
|
// Default values for certain options
|
||||||
public static final int UNSPECIFIED_PORT = -1;
|
public static final int UNSPECIFIED_PORT = -1;
|
||||||
@ -204,6 +206,8 @@ public class Config {
|
|||||||
public final boolean republishMailboxEntries;
|
public final boolean republishMailboxEntries;
|
||||||
public final boolean bypassMempoolValidation;
|
public final boolean bypassMempoolValidation;
|
||||||
public final boolean passwordRequired;
|
public final boolean passwordRequired;
|
||||||
|
public final boolean updateXmrBinaries;
|
||||||
|
public final String xmrBlockchainPath;
|
||||||
|
|
||||||
// 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;
|
||||||
@ -621,6 +625,20 @@ public class Config {
|
|||||||
.ofType(boolean.class)
|
.ofType(boolean.class)
|
||||||
.defaultsTo(false);
|
.defaultsTo(false);
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<Boolean> updateXmrBinariesOpt =
|
||||||
|
parser.accepts(UPDATE_XMR_BINARIES,
|
||||||
|
"Update Monero binaries if applicable")
|
||||||
|
.withRequiredArg()
|
||||||
|
.ofType(boolean.class)
|
||||||
|
.defaultsTo(true);
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<String> xmrBlockchainPathOpt =
|
||||||
|
parser.accepts(XMR_BLOCKCHAIN_PATH,
|
||||||
|
"Path to Monero blockchain when using local Monero node")
|
||||||
|
.withRequiredArg()
|
||||||
|
.ofType(String.class)
|
||||||
|
.defaultsTo("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CompositeOptionSet options = new CompositeOptionSet();
|
CompositeOptionSet options = new CompositeOptionSet();
|
||||||
|
|
||||||
@ -733,6 +751,8 @@ public class Config {
|
|||||||
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
||||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||||
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
||||||
|
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
|
||||||
|
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
|
||||||
} 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),
|
||||||
@ -742,11 +762,11 @@ public class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create all appDataDir subdirectories and assign to their respective properties
|
// Create all appDataDir subdirectories and assign to their respective properties
|
||||||
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
|
File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
|
||||||
this.keyStorageDir = mkdir(btcNetworkDir, "keys");
|
this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
|
||||||
this.storageDir = mkdir(btcNetworkDir, "db");
|
this.storageDir = mkdir(xmrNetworkDir, "db");
|
||||||
this.torDir = mkdir(btcNetworkDir, "tor");
|
this.torDir = mkdir(xmrNetworkDir, "tor");
|
||||||
this.walletDir = mkdir(btcNetworkDir, "wallet");
|
this.walletDir = mkdir(xmrNetworkDir, "wallet");
|
||||||
|
|
||||||
// Assign values to special-case static fields
|
// Assign values to special-case static fields
|
||||||
APP_DATA_DIR_VALUE = appDataDir;
|
APP_DATA_DIR_VALUE = appDataDir;
|
||||||
|
@ -110,7 +110,7 @@ public final class KeyRing {
|
|||||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||||
*/
|
*/
|
||||||
public void generateKeys(String password) {
|
public void generateKeys(String password) {
|
||||||
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys");
|
||||||
symmetricKey = Encryption.generateSecretKey(256);
|
symmetricKey = Encryption.generateSecretKey(256);
|
||||||
signatureKeyPair = Sig.generateKeyPair();
|
signatureKeyPair = Sig.generateKeyPair();
|
||||||
encryptionKeyPair = Encryption.generateKeyPair();
|
encryptionKeyPair = Encryption.generateKeyPair();
|
||||||
|
@ -243,6 +243,11 @@ public class KeyStorage {
|
|||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
storageDir.mkdirs();
|
storageDir.mkdirs();
|
||||||
|
|
||||||
|
// password must be ascii
|
||||||
|
if (password != null && !password.matches("\\p{ASCII}*")) {
|
||||||
|
throw new IllegalArgumentException("Password must be ASCII.");
|
||||||
|
}
|
||||||
|
|
||||||
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
|
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
|
||||||
var passwordChars = password == null ? new char[0] : password.toCharArray();
|
var passwordChars = password == null ? new char[0] : password.toCharArray();
|
||||||
try {
|
try {
|
||||||
|
@ -32,6 +32,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -76,11 +77,11 @@ public class FileUtil {
|
|||||||
|
|
||||||
public static List<File> getBackupFiles(File dir, String fileName) {
|
public static List<File> getBackupFiles(File dir, String fileName) {
|
||||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
|
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
|
||||||
if (!backupDir.exists()) return null;
|
if (!backupDir.exists()) return new ArrayList<File>();
|
||||||
String dirName = "backups_" + fileName;
|
String dirName = "backups_" + fileName;
|
||||||
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
|
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
|
||||||
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
|
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
|
||||||
if (!backupFileDir.exists()) return null;
|
if (!backupFileDir.exists()) return new ArrayList<File>();
|
||||||
File[] files = backupFileDir.listFiles();
|
File[] files = backupFileDir.listFiles();
|
||||||
return Arrays.asList(files);
|
return Arrays.asList(files);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection;
|
|||||||
import haveno.core.offer.OfferRestrictions;
|
import haveno.core.offer.OfferRestrictions;
|
||||||
import haveno.core.payment.ChargeBackRisk;
|
import haveno.core.payment.ChargeBackRisk;
|
||||||
import haveno.core.payment.PaymentAccount;
|
import haveno.core.payment.PaymentAccount;
|
||||||
|
import haveno.core.payment.TradeLimits;
|
||||||
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.support.dispute.Dispute;
|
import haveno.core.support.dispute.Dispute;
|
||||||
@ -498,10 +499,15 @@ public class AccountAgeWitnessService {
|
|||||||
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
|
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) {
|
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) {
|
||||||
if (paymentAccount == null)
|
if (paymentAccount == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
if (buyerAsTakerWithoutDeposit) {
|
||||||
|
TradeLimits tradeLimits = new TradeLimits();
|
||||||
|
return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
|
||||||
|
}
|
||||||
|
|
||||||
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
|
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
|
||||||
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
|
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
|
||||||
if (hasTradeLimitException(accountAgeWitness)) {
|
if (hasTradeLimitException(accountAgeWitness)) {
|
||||||
|
@ -239,8 +239,8 @@ public class CoreApi {
|
|||||||
xmrConnectionService.stopCheckingConnection();
|
xmrConnectionService.stopCheckingConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneroRpcConnection getBestAvailableXmrConnection() {
|
public MoneroRpcConnection getBestXmrConnection() {
|
||||||
return xmrConnectionService.getBestAvailableConnection();
|
return xmrConnectionService.getBestConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
|
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
|
||||||
@ -419,10 +419,14 @@ public class CoreApi {
|
|||||||
double marketPriceMargin,
|
double marketPriceMargin,
|
||||||
long amountAsLong,
|
long amountAsLong,
|
||||||
long minAmountAsLong,
|
long minAmountAsLong,
|
||||||
double buyerSecurityDeposit,
|
double securityDepositPct,
|
||||||
String triggerPriceAsString,
|
String triggerPriceAsString,
|
||||||
boolean reserveExactAmount,
|
boolean reserveExactAmount,
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
|
boolean isPrivateOffer,
|
||||||
|
boolean buyerAsTakerWithoutDeposit,
|
||||||
|
String extraInfo,
|
||||||
|
String sourceOfferId,
|
||||||
Consumer<Offer> resultHandler,
|
Consumer<Offer> resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
coreOffersService.postOffer(currencyCode,
|
coreOffersService.postOffer(currencyCode,
|
||||||
@ -432,10 +436,14 @@ public class CoreApi {
|
|||||||
marketPriceMargin,
|
marketPriceMargin,
|
||||||
amountAsLong,
|
amountAsLong,
|
||||||
minAmountAsLong,
|
minAmountAsLong,
|
||||||
buyerSecurityDeposit,
|
securityDepositPct,
|
||||||
triggerPriceAsString,
|
triggerPriceAsString,
|
||||||
reserveExactAmount,
|
reserveExactAmount,
|
||||||
paymentAccountId,
|
paymentAccountId,
|
||||||
|
isPrivateOffer,
|
||||||
|
buyerAsTakerWithoutDeposit,
|
||||||
|
extraInfo,
|
||||||
|
sourceOfferId,
|
||||||
resultHandler,
|
resultHandler,
|
||||||
errorMessageHandler);
|
errorMessageHandler);
|
||||||
}
|
}
|
||||||
@ -448,8 +456,11 @@ public class CoreApi {
|
|||||||
double marketPriceMargin,
|
double marketPriceMargin,
|
||||||
BigInteger amount,
|
BigInteger amount,
|
||||||
BigInteger minAmount,
|
BigInteger minAmount,
|
||||||
double buyerSecurityDeposit,
|
double securityDepositPct,
|
||||||
PaymentAccount paymentAccount) {
|
PaymentAccount paymentAccount,
|
||||||
|
boolean isPrivateOffer,
|
||||||
|
boolean buyerAsTakerWithoutDeposit,
|
||||||
|
String extraInfo) {
|
||||||
return coreOffersService.editOffer(offerId,
|
return coreOffersService.editOffer(offerId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
direction,
|
direction,
|
||||||
@ -458,8 +469,11 @@ public class CoreApi {
|
|||||||
marketPriceMargin,
|
marketPriceMargin,
|
||||||
amount,
|
amount,
|
||||||
minAmount,
|
minAmount,
|
||||||
buyerSecurityDeposit,
|
securityDepositPct,
|
||||||
paymentAccount);
|
paymentAccount,
|
||||||
|
isPrivateOffer,
|
||||||
|
buyerAsTakerWithoutDeposit,
|
||||||
|
extraInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
@ -535,9 +549,11 @@ public class CoreApi {
|
|||||||
public void takeOffer(String offerId,
|
public void takeOffer(String offerId,
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
long amountAsLong,
|
long amountAsLong,
|
||||||
|
String challenge,
|
||||||
Consumer<Trade> resultHandler,
|
Consumer<Trade> resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
Offer offer = coreOffersService.getOffer(offerId);
|
Offer offer = coreOffersService.getOffer(offerId);
|
||||||
|
offer.setChallenge(challenge);
|
||||||
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
|
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CoreDisputesService {
|
public class CoreDisputesService {
|
||||||
|
|
||||||
public enum DisputePayout {
|
// TODO: persist in DisputeResult?
|
||||||
|
public enum PayoutSuggestion {
|
||||||
BUYER_GETS_TRADE_AMOUNT,
|
BUYER_GETS_TRADE_AMOUNT,
|
||||||
BUYER_GETS_ALL, // used in desktop
|
BUYER_GETS_ALL,
|
||||||
SELLER_GETS_TRADE_AMOUNT,
|
SELLER_GETS_TRADE_AMOUNT,
|
||||||
SELLER_GETS_ALL, // used in desktop
|
SELLER_GETS_ALL,
|
||||||
CUSTOM
|
CUSTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,17 +173,17 @@ public class CoreDisputesService {
|
|||||||
// create dispute result
|
// create dispute result
|
||||||
var closeDate = new Date();
|
var closeDate = new Date();
|
||||||
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
|
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
|
||||||
DisputePayout payout;
|
PayoutSuggestion payoutSuggestion;
|
||||||
if (customWinnerAmount > 0) {
|
if (customWinnerAmount > 0) {
|
||||||
payout = DisputePayout.CUSTOM;
|
payoutSuggestion = PayoutSuggestion.CUSTOM;
|
||||||
} else if (winner == DisputeResult.Winner.BUYER) {
|
} else if (winner == DisputeResult.Winner.BUYER) {
|
||||||
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
|
payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
|
||||||
} else if (winner == DisputeResult.Winner.SELLER) {
|
} else if (winner == DisputeResult.Winner.SELLER) {
|
||||||
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
|
payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
||||||
}
|
}
|
||||||
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
|
applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
|
||||||
|
|
||||||
// close winning dispute ticket
|
// close winning dispute ticket
|
||||||
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
|
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
|
||||||
@ -227,26 +228,26 @@ public class CoreDisputesService {
|
|||||||
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
|
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
|
||||||
* receives the remaining amount minus the mining fee.
|
* receives the remaining amount minus the mining fee.
|
||||||
*/
|
*/
|
||||||
public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
|
public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
|
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
|
||||||
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
|
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
|
||||||
BigInteger tradeAmount = contract.getTradeAmount();
|
BigInteger tradeAmount = contract.getTradeAmount();
|
||||||
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
|
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
|
||||||
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
|
if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) {
|
||||||
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
|
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
|
||||||
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
|
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
|
||||||
} else if (payout == DisputePayout.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);
|
||||||
} else if (payout == DisputePayout.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 (payout == DisputePayout.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));
|
||||||
} else if (payout == DisputePayout.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();
|
||||||
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
|
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
|
||||||
|
@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply;
|
|||||||
import static haveno.common.util.MathUtils.roundDoubleToLong;
|
import static haveno.common.util.MathUtils.roundDoubleToLong;
|
||||||
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
|
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
|
||||||
import haveno.core.locale.CurrencyUtil;
|
import haveno.core.locale.CurrencyUtil;
|
||||||
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.monetary.CryptoMoney;
|
import haveno.core.monetary.CryptoMoney;
|
||||||
import haveno.core.monetary.Price;
|
import haveno.core.monetary.Price;
|
||||||
import haveno.core.monetary.TraditionalMoney;
|
import haveno.core.monetary.TraditionalMoney;
|
||||||
@ -66,9 +67,7 @@ import java.math.BigInteger;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import static java.util.Comparator.comparing;
|
import static java.util.Comparator.comparing;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -124,7 +123,6 @@ public class CoreOffersService {
|
|||||||
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
|
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
|
|
||||||
return offers;
|
return offers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,12 +141,9 @@ public class CoreOffersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<OpenOffer> getMyOffers() {
|
List<OpenOffer> getMyOffers() {
|
||||||
List<OpenOffer> offers = openOfferManager.getOpenOffers().stream()
|
return openOfferManager.getOpenOffers().stream()
|
||||||
.filter(o -> o.getOffer().isMyOffer(keyRing))
|
.filter(o -> o.getOffer().isMyOffer(keyRing))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Set<Offer> offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images
|
|
||||||
Set<String> offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet());
|
|
||||||
return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
|
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
|
||||||
@ -159,7 +154,7 @@ public class CoreOffersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OpenOffer getMyOffer(String id) {
|
OpenOffer getMyOffer(String id) {
|
||||||
return openOfferManager.getOpenOfferById(id)
|
return openOfferManager.getOpenOffer(id)
|
||||||
.filter(open -> open.getOffer().isMyOffer(keyRing))
|
.filter(open -> open.getOffer().isMyOffer(keyRing))
|
||||||
.orElseThrow(() ->
|
.orElseThrow(() ->
|
||||||
new IllegalStateException(format("openoffer with id '%s' not found", id)));
|
new IllegalStateException(format("openoffer with id '%s' not found", id)));
|
||||||
@ -172,19 +167,38 @@ public class CoreOffersService {
|
|||||||
double marketPriceMargin,
|
double marketPriceMargin,
|
||||||
long amountAsLong,
|
long amountAsLong,
|
||||||
long minAmountAsLong,
|
long minAmountAsLong,
|
||||||
double securityDeposit,
|
double securityDepositPct,
|
||||||
String triggerPriceAsString,
|
String triggerPriceAsString,
|
||||||
boolean reserveExactAmount,
|
boolean reserveExactAmount,
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
|
boolean isPrivateOffer,
|
||||||
|
boolean buyerAsTakerWithoutDeposit,
|
||||||
|
String extraInfo,
|
||||||
|
String sourceOfferId,
|
||||||
Consumer<Offer> resultHandler,
|
Consumer<Offer> resultHandler,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
coreWalletsService.verifyWalletsAreAvailable();
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
|
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
|
||||||
if (paymentAccount == null)
|
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
|
||||||
throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
|
|
||||||
|
|
||||||
|
// clone offer if sourceOfferId given
|
||||||
|
if (!sourceOfferId.isEmpty()) {
|
||||||
|
cloneOffer(sourceOfferId,
|
||||||
|
currencyCode,
|
||||||
|
priceAsString,
|
||||||
|
useMarketBasedPrice,
|
||||||
|
marketPriceMargin,
|
||||||
|
triggerPriceAsString,
|
||||||
|
paymentAccountId,
|
||||||
|
extraInfo,
|
||||||
|
resultHandler,
|
||||||
|
errorMessageHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new offer
|
||||||
String upperCaseCurrencyCode = currencyCode.toUpperCase();
|
String upperCaseCurrencyCode = currencyCode.toUpperCase();
|
||||||
String offerId = createOfferService.getRandomOfferId();
|
String offerId = createOfferService.getRandomOfferId();
|
||||||
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
|
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
|
||||||
@ -199,22 +213,78 @@ public class CoreOffersService {
|
|||||||
price,
|
price,
|
||||||
useMarketBasedPrice,
|
useMarketBasedPrice,
|
||||||
exactMultiply(marketPriceMargin, 0.01),
|
exactMultiply(marketPriceMargin, 0.01),
|
||||||
securityDeposit,
|
securityDepositPct,
|
||||||
paymentAccount);
|
paymentAccount,
|
||||||
|
isPrivateOffer,
|
||||||
|
buyerAsTakerWithoutDeposit,
|
||||||
|
extraInfo);
|
||||||
|
|
||||||
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
||||||
|
|
||||||
// We don't support atm funding from external wallet to keep it simple.
|
|
||||||
boolean useSavingsWallet = true;
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
placeOffer(offer,
|
placeOffer(offer,
|
||||||
triggerPriceAsString,
|
triggerPriceAsString,
|
||||||
useSavingsWallet,
|
true,
|
||||||
reserveExactAmount,
|
reserveExactAmount,
|
||||||
|
null,
|
||||||
transaction -> resultHandler.accept(offer),
|
transaction -> resultHandler.accept(offer),
|
||||||
errorMessageHandler);
|
errorMessageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cloneOffer(String sourceOfferId,
|
||||||
|
String currencyCode,
|
||||||
|
String priceAsString,
|
||||||
|
boolean useMarketBasedPrice,
|
||||||
|
double marketPriceMargin,
|
||||||
|
String triggerPriceAsString,
|
||||||
|
String paymentAccountId,
|
||||||
|
String extraInfo,
|
||||||
|
Consumer<Offer> resultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
|
|
||||||
|
// get source offer
|
||||||
|
OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId);
|
||||||
|
Offer sourceOffer = sourceOpenOffer.getOffer();
|
||||||
|
|
||||||
|
// get trade currency (default source currency)
|
||||||
|
if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode();
|
||||||
|
if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode();
|
||||||
|
String upperCaseCurrencyCode = currencyCode.toUpperCase();
|
||||||
|
|
||||||
|
// get price (default source price)
|
||||||
|
Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString);
|
||||||
|
if (price == null) useMarketBasedPrice = true;
|
||||||
|
|
||||||
|
// get payment account
|
||||||
|
if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
|
||||||
|
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
|
||||||
|
if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
|
||||||
|
|
||||||
|
// get extra info
|
||||||
|
if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
|
||||||
|
|
||||||
|
// create cloned offer
|
||||||
|
Offer offer = createOfferService.createClonedOffer(sourceOffer,
|
||||||
|
upperCaseCurrencyCode,
|
||||||
|
price,
|
||||||
|
useMarketBasedPrice,
|
||||||
|
exactMultiply(marketPriceMargin, 0.01),
|
||||||
|
paymentAccount,
|
||||||
|
extraInfo);
|
||||||
|
|
||||||
|
// verify cloned offer
|
||||||
|
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
||||||
|
|
||||||
|
// place offer
|
||||||
|
placeOffer(offer,
|
||||||
|
triggerPriceAsString,
|
||||||
|
true,
|
||||||
|
false, // ignored when cloning
|
||||||
|
sourceOfferId,
|
||||||
|
transaction -> resultHandler.accept(offer),
|
||||||
|
errorMessageHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this implementation is missing; implement.
|
||||||
Offer editOffer(String offerId,
|
Offer editOffer(String offerId,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
OfferDirection direction,
|
OfferDirection direction,
|
||||||
@ -223,8 +293,11 @@ public class CoreOffersService {
|
|||||||
double marketPriceMargin,
|
double marketPriceMargin,
|
||||||
BigInteger amount,
|
BigInteger amount,
|
||||||
BigInteger minAmount,
|
BigInteger minAmount,
|
||||||
double buyerSecurityDeposit,
|
double securityDepositPct,
|
||||||
PaymentAccount paymentAccount) {
|
PaymentAccount paymentAccount,
|
||||||
|
boolean isPrivateOffer,
|
||||||
|
boolean buyerAsTakerWithoutDeposit,
|
||||||
|
String extraInfo) {
|
||||||
return createOfferService.createAndGetOffer(offerId,
|
return createOfferService.createAndGetOffer(offerId,
|
||||||
direction,
|
direction,
|
||||||
currencyCode.toUpperCase(),
|
currencyCode.toUpperCase(),
|
||||||
@ -233,8 +306,11 @@ public class CoreOffersService {
|
|||||||
price,
|
price,
|
||||||
useMarketBasedPrice,
|
useMarketBasedPrice,
|
||||||
exactMultiply(marketPriceMargin, 0.01),
|
exactMultiply(marketPriceMargin, 0.01),
|
||||||
buyerSecurityDeposit,
|
securityDepositPct,
|
||||||
paymentAccount);
|
paymentAccount,
|
||||||
|
isPrivateOffer,
|
||||||
|
buyerAsTakerWithoutDeposit,
|
||||||
|
extraInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
@ -244,26 +320,6 @@ public class CoreOffersService {
|
|||||||
|
|
||||||
// -------------------------- PRIVATE HELPERS -----------------------------
|
// -------------------------- PRIVATE HELPERS -----------------------------
|
||||||
|
|
||||||
private Set<Offer> getOffersWithDuplicateKeyImages(List<Offer> offers) {
|
|
||||||
Set<Offer> duplicateFundedOffers = new HashSet<Offer>();
|
|
||||||
Set<String> seenKeyImages = new HashSet<String>();
|
|
||||||
for (Offer offer : offers) {
|
|
||||||
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
|
|
||||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
|
||||||
if (!seenKeyImages.add(keyImage)) {
|
|
||||||
for (Offer offer2 : offers) {
|
|
||||||
if (offer == offer2) continue;
|
|
||||||
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
|
||||||
log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
|
|
||||||
duplicateFundedOffers.add(offer2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return duplicateFundedOffers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
|
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
|
||||||
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
|
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
|
||||||
String error = format("cannot create %s offer with payment account %s",
|
String error = format("cannot create %s offer with payment account %s",
|
||||||
@ -277,6 +333,7 @@ public class CoreOffersService {
|
|||||||
String triggerPriceAsString,
|
String triggerPriceAsString,
|
||||||
boolean useSavingsWallet,
|
boolean useSavingsWallet,
|
||||||
boolean reserveExactAmount,
|
boolean reserveExactAmount,
|
||||||
|
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.getCurrencyCode());
|
||||||
@ -285,6 +342,7 @@ public class CoreOffersService {
|
|||||||
triggerPriceAsLong,
|
triggerPriceAsLong,
|
||||||
reserveExactAmount,
|
reserveExactAmount,
|
||||||
true,
|
true,
|
||||||
|
sourceOfferId,
|
||||||
resultHandler::accept,
|
resultHandler::accept,
|
||||||
errorMessageHandler);
|
errorMessageHandler);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage;
|
|||||||
import haveno.core.support.traderchat.TradeChatSession;
|
import haveno.core.support.traderchat.TradeChatSession;
|
||||||
import haveno.core.support.traderchat.TraderChatManager;
|
import haveno.core.support.traderchat.TraderChatManager;
|
||||||
import haveno.core.trade.ClosedTradableManager;
|
import haveno.core.trade.ClosedTradableManager;
|
||||||
import haveno.core.trade.Tradable;
|
|
||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.TradeManager;
|
import haveno.core.trade.TradeManager;
|
||||||
import haveno.core.trade.TradeUtil;
|
import haveno.core.trade.TradeUtil;
|
||||||
@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol;
|
|||||||
import haveno.core.trade.protocol.SellerProtocol;
|
import haveno.core.trade.protocol.SellerProtocol;
|
||||||
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.util.validation.BtcAddressValidator;
|
|
||||||
import haveno.core.xmr.model.AddressEntry;
|
|
||||||
import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT;
|
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -68,7 +64,6 @@ import java.util.function.Consumer;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -84,7 +79,6 @@ class CoreTradesService {
|
|||||||
private final TakeOfferModel takeOfferModel;
|
private final TakeOfferModel takeOfferModel;
|
||||||
private final TradeManager tradeManager;
|
private final TradeManager tradeManager;
|
||||||
private final TraderChatManager traderChatManager;
|
private final TraderChatManager traderChatManager;
|
||||||
private final TradeUtil tradeUtil;
|
|
||||||
private final OfferUtil offerUtil;
|
private final OfferUtil offerUtil;
|
||||||
private final User user;
|
private final User user;
|
||||||
|
|
||||||
@ -106,7 +100,6 @@ class CoreTradesService {
|
|||||||
this.takeOfferModel = takeOfferModel;
|
this.takeOfferModel = takeOfferModel;
|
||||||
this.tradeManager = tradeManager;
|
this.tradeManager = tradeManager;
|
||||||
this.traderChatManager = traderChatManager;
|
this.traderChatManager = traderChatManager;
|
||||||
this.tradeUtil = tradeUtil;
|
|
||||||
this.offerUtil = offerUtil;
|
this.offerUtil = offerUtil;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
@ -132,7 +125,7 @@ class CoreTradesService {
|
|||||||
// 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.getCurrencyCode();
|
||||||
OfferDirection direction = offer.getOfferPayload().getDirection();
|
OfferDirection direction = offer.getOfferPayload().getDirection();
|
||||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
|
long maxTradeLimit = 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(), maxTradeLimit);
|
||||||
@ -206,7 +199,7 @@ class CoreTradesService {
|
|||||||
String getTradeRole(String tradeId) {
|
String getTradeRole(String tradeId) {
|
||||||
coreWalletsService.verifyWalletsAreAvailable();
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
return tradeUtil.getRole(getTrade(tradeId));
|
return TradeUtil.getRole(getTrade(tradeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
Trade getTrade(String tradeId) {
|
Trade getTrade(String tradeId) {
|
||||||
@ -223,8 +216,7 @@ class CoreTradesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Trade> getClosedTrade(String tradeId) {
|
private Optional<Trade> getClosedTrade(String tradeId) {
|
||||||
Optional<Tradable> tradable = closedTradableManager.getTradeById(tradeId);
|
return closedTradableManager.getTradeById(tradeId);
|
||||||
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Trade> getTrades() {
|
List<Trade> getTrades() {
|
||||||
@ -267,40 +259,9 @@ class CoreTradesService {
|
|||||||
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
|
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
|
|
||||||
// TODO This and identical logic should be refactored into TradeUtil.
|
|
||||||
try {
|
|
||||||
return btcWalletService.getFeeEstimationTransaction(fromAddress,
|
|
||||||
toAddress,
|
|
||||||
amount,
|
|
||||||
TRADE_PAYOUT).getFee();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("", ex);
|
|
||||||
throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throws a RuntimeException trade is already closed.
|
// Throws a RuntimeException trade is already closed.
|
||||||
private void verifyTradeIsNotClosed(String tradeId) {
|
private void verifyTradeIsNotClosed(String tradeId) {
|
||||||
if (getClosedTrade(tradeId).isPresent())
|
if (getClosedTrade(tradeId).isPresent())
|
||||||
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
|
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throws a RuntimeException if address is not valid.
|
|
||||||
private void verifyIsValidBTCAddress(String address) {
|
|
||||||
try {
|
|
||||||
new BtcAddressValidator().validate(address);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
log.error("", t);
|
|
||||||
throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throws a RuntimeException if address has a zero balance.
|
|
||||||
private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
|
|
||||||
Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
|
|
||||||
if (fromAddressBalance.isZero())
|
|
||||||
throw new IllegalStateException(format("funds already withdrawn from address '%s'",
|
|
||||||
fromAddressEntry.getAddressString()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
|
|||||||
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
|
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
|
||||||
import haveno.core.xmr.setup.DownloadListener;
|
import haveno.core.xmr.setup.DownloadListener;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
|
import haveno.core.xmr.wallet.XmrKeyImagePoller;
|
||||||
import haveno.network.Socks5ProxyProvider;
|
import haveno.network.Socks5ProxyProvider;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import haveno.network.p2p.P2PServiceListener;
|
import haveno.network.p2p.P2PServiceListener;
|
||||||
@ -40,7 +41,6 @@ import java.util.Collection;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
|
||||||
@ -64,7 +64,6 @@ import monero.common.MoneroRpcConnection;
|
|||||||
import monero.common.TaskLooper;
|
import monero.common.TaskLooper;
|
||||||
import monero.daemon.MoneroDaemonRpc;
|
import monero.daemon.MoneroDaemonRpc;
|
||||||
import monero.daemon.model.MoneroDaemonInfo;
|
import monero.daemon.model.MoneroDaemonInfo;
|
||||||
import monero.daemon.model.MoneroPeer;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -73,6 +72,14 @@ public final class XmrConnectionService {
|
|||||||
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
||||||
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
|
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
|
||||||
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
||||||
|
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
||||||
|
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
||||||
|
|
||||||
|
public enum XmrConnectionFallbackType {
|
||||||
|
LOCAL,
|
||||||
|
CUSTOM,
|
||||||
|
PROVIDED
|
||||||
|
}
|
||||||
|
|
||||||
private final Object lock = new Object();
|
private final Object lock = new Object();
|
||||||
private final Object pollLock = new Object();
|
private final Object pollLock = new Object();
|
||||||
@ -85,12 +92,14 @@ public final class XmrConnectionService {
|
|||||||
private final XmrLocalNode xmrLocalNode;
|
private final XmrLocalNode xmrLocalNode;
|
||||||
private final MoneroConnectionManager connectionManager;
|
private final MoneroConnectionManager connectionManager;
|
||||||
private final EncryptedConnectionList connectionList;
|
private final EncryptedConnectionList connectionList;
|
||||||
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
|
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
|
||||||
|
private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
|
||||||
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
|
private final ObjectProperty<MoneroRpcConnection> connectionProperty = new SimpleObjectProperty<>();
|
||||||
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
|
|
||||||
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<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
|
||||||
|
@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);
|
||||||
private Socks5ProxyProvider socks5ProxyProvider;
|
private Socks5ProxyProvider socks5ProxyProvider;
|
||||||
@ -101,6 +110,7 @@ public final class XmrConnectionService {
|
|||||||
private Boolean isConnected = false;
|
private Boolean isConnected = false;
|
||||||
@Getter
|
@Getter
|
||||||
private MoneroDaemonInfo lastInfo;
|
private MoneroDaemonInfo lastInfo;
|
||||||
|
private Long lastFallbackInvocation;
|
||||||
private Long lastLogPollErrorTimestamp;
|
private Long lastLogPollErrorTimestamp;
|
||||||
private long lastLogDaemonNotSyncedTimestamp;
|
private long lastLogDaemonNotSyncedTimestamp;
|
||||||
private Long syncStartHeight;
|
private Long syncStartHeight;
|
||||||
@ -109,6 +119,7 @@ public final class XmrConnectionService {
|
|||||||
@Getter
|
@Getter
|
||||||
private boolean isShutDownStarted;
|
private boolean isShutDownStarted;
|
||||||
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
||||||
|
private XmrKeyImagePoller keyImagePoller;
|
||||||
|
|
||||||
// connection switching
|
// connection switching
|
||||||
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
|
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
|
||||||
@ -117,6 +128,9 @@ public final class XmrConnectionService {
|
|||||||
private int numRequestsLastMinute;
|
private int numRequestsLastMinute;
|
||||||
private long lastSwitchTimestamp;
|
private long lastSwitchTimestamp;
|
||||||
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 boolean fallbackApplied;
|
||||||
|
private boolean usedSyncingLocalNodeBeforeStartup;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public XmrConnectionService(P2PService p2PService,
|
public XmrConnectionService(P2PService p2PService,
|
||||||
@ -144,7 +158,13 @@ public final class XmrConnectionService {
|
|||||||
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
ThreadUtils.submitToPool(() -> {
|
||||||
|
try {
|
||||||
initialize();
|
initialize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onHiddenServicePublished() {}
|
public void onHiddenServicePublished() {}
|
||||||
@ -250,18 +270,29 @@ public final class XmrConnectionService {
|
|||||||
updatePolling();
|
updatePolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneroRpcConnection getBestAvailableConnection() {
|
public MoneroRpcConnection getBestConnection() {
|
||||||
accountService.checkAccountOpen();
|
return getBestConnection(new ArrayList<MoneroRpcConnection>());
|
||||||
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
|
|
||||||
addLocalNodeIfIgnored(ignoredConnections);
|
|
||||||
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) {
|
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
|
|
||||||
|
// user needs to authorize fallback on startup after using locally synced node
|
||||||
|
if (fallbackRequiredBeforeConnectionSwitch()) {
|
||||||
|
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get best connection
|
||||||
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
|
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
|
||||||
addLocalNodeIfIgnored(ignoredConnectionsSet);
|
addLocalNodeIfIgnored(ignoredConnectionsSet);
|
||||||
return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0]));
|
MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections
|
||||||
|
if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0);
|
||||||
|
return bestConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean fallbackRequiredBeforeConnectionSwitch() {
|
||||||
|
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||||
@ -273,7 +304,7 @@ public final class XmrConnectionService {
|
|||||||
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
|
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
MoneroRpcConnection bestConnection = getBestConnection();
|
||||||
if (bestConnection != null) setConnection(bestConnection);
|
if (bestConnection != null) setConnection(bestConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +355,7 @@ public final class XmrConnectionService {
|
|||||||
if (currentConnection != null) excludedConnections.add(currentConnection);
|
if (currentConnection != null) excludedConnections.add(currentConnection);
|
||||||
|
|
||||||
// get connection to switch to
|
// get connection to switch to
|
||||||
MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections);
|
MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
|
||||||
|
|
||||||
// remove from excluded connections after period
|
// remove from excluded connections after period
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
@ -332,7 +363,7 @@ public final class XmrConnectionService {
|
|||||||
}, EXCLUDE_CONNECTION_SECONDS);
|
}, EXCLUDE_CONNECTION_SECONDS);
|
||||||
|
|
||||||
// return if no connection to switch to
|
// return if no connection to switch to
|
||||||
if (bestConnection == null) {
|
if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
|
||||||
log.warn("No connection to switch to");
|
log.warn("No connection to switch to");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -388,14 +419,25 @@ public final class XmrConnectionService {
|
|||||||
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
|
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------- APP METHODS ------------------------------
|
public XmrKeyImagePoller getKeyImagePoller() {
|
||||||
|
synchronized (lock) {
|
||||||
public ReadOnlyIntegerProperty numPeersProperty() {
|
if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
|
||||||
return numPeers;
|
return keyImagePoller;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
|
private long getKeyImageRefreshPeriodMs() {
|
||||||
return peers;
|
return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------- APP METHODS ------------------------------
|
||||||
|
|
||||||
|
public ReadOnlyIntegerProperty numConnectionsProperty() {
|
||||||
|
return numConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObjectProperty<List<MoneroRpcConnection>> connectionsProperty() {
|
||||||
|
return connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() {
|
public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() {
|
||||||
@ -403,7 +445,7 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSufficientPeersForBroadcast() {
|
public boolean hasSufficientPeersForBroadcast() {
|
||||||
return numPeers.get() >= getMinBroadcastConnections();
|
return numConnections.get() >= getMinBroadcastConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LongProperty chainHeightProperty() {
|
public LongProperty chainHeightProperty() {
|
||||||
@ -426,6 +468,24 @@ public final class XmrConnectionService {
|
|||||||
return numUpdates;
|
return numUpdates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fallbackToBestConnection() {
|
||||||
|
if (isShutDownStarted) return;
|
||||||
|
fallbackApplied = true;
|
||||||
|
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||||
|
log.warn("Falling back to public nodes");
|
||||||
|
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
||||||
|
initializeConnections();
|
||||||
|
} else {
|
||||||
|
log.warn("Falling back to provided nodes");
|
||||||
|
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
|
||||||
|
initializeConnections();
|
||||||
|
if (getConnection() == null) {
|
||||||
|
log.warn("No provided nodes available, falling back to public nodes");
|
||||||
|
fallbackToBestConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------- HELPERS --------------------------------
|
// ------------------------------- HELPERS --------------------------------
|
||||||
|
|
||||||
private void doneDownload() {
|
private void doneDownload() {
|
||||||
@ -460,6 +520,13 @@ public final class XmrConnectionService {
|
|||||||
|
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
|
|
||||||
|
// initialize key image poller
|
||||||
|
getKeyImagePoller();
|
||||||
|
new Thread(() -> {
|
||||||
|
HavenoUtils.waitFor(20000);
|
||||||
|
keyImagePoller.poll(); // TODO: keep or remove first poll?s
|
||||||
|
}).start();
|
||||||
|
|
||||||
// initialize connections
|
// initialize connections
|
||||||
initializeConnections();
|
initializeConnections();
|
||||||
|
|
||||||
@ -526,8 +593,13 @@ public final class XmrConnectionService {
|
|||||||
// update connection
|
// update connection
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
setConnection(connection.getUri());
|
setConnection(connection.getUri());
|
||||||
|
|
||||||
|
// reset error connecting to local node
|
||||||
|
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
|
||||||
|
connectionServiceFallbackType.set(null);
|
||||||
|
}
|
||||||
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
||||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
MoneroRpcConnection bestConnection = getBestConnection();
|
||||||
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
|
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,7 +607,7 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// restore connections
|
// restore connections
|
||||||
if ("".equals(config.xmrNode)) {
|
if (!isFixedConnection()) {
|
||||||
|
|
||||||
// load previous or default connections
|
// load previous or default connections
|
||||||
if (coreContext.isApiUser()) {
|
if (coreContext.isApiUser()) {
|
||||||
@ -547,9 +619,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);
|
||||||
@ -560,9 +634,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);
|
||||||
@ -571,15 +647,17 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// restore last connection
|
// restore last connection
|
||||||
if (isFixedConnection()) {
|
if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
|
||||||
if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size());
|
|
||||||
connectionManager.setConnection(getConnections().get(0));
|
|
||||||
} else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
|
|
||||||
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) {
|
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) {
|
||||||
connectionManager.setConnection(connectionList.getCurrentConnectionUri().get());
|
connectionManager.setConnection(connectionList.getCurrentConnectionUri().get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
@ -590,12 +668,9 @@ public final class XmrConnectionService {
|
|||||||
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
|
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
|
||||||
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
|
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
|
||||||
|
|
||||||
// start local node if applicable
|
|
||||||
maybeStartLocalNode();
|
|
||||||
|
|
||||||
// update connection
|
// update connection
|
||||||
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
|
if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
|
||||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
MoneroRpcConnection bestConnection = getBestConnection();
|
||||||
if (bestConnection != null) setConnection(bestConnection);
|
if (bestConnection != null) setConnection(bestConnection);
|
||||||
}
|
}
|
||||||
} else if (!isInitialized) {
|
} else if (!isInitialized) {
|
||||||
@ -605,9 +680,6 @@ public final class XmrConnectionService {
|
|||||||
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
|
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
|
||||||
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
|
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
|
||||||
connectionManager.setConnection(connection);
|
connectionManager.setConnection(connection);
|
||||||
|
|
||||||
// start local node if applicable
|
|
||||||
maybeStartLocalNode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// register connection listener
|
// register connection listener
|
||||||
@ -616,30 +688,26 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// notify initial connection
|
// notify initial connection
|
||||||
|
lastRefreshPeriodMs = getRefreshPeriodMs();
|
||||||
onConnectionChanged(connectionManager.getConnection());
|
onConnectionChanged(connectionManager.getConnection());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeStartLocalNode() {
|
public void startLocalNode() throws Exception {
|
||||||
|
|
||||||
// skip if seed node
|
// cannot start local node as seed node
|
||||||
if (HavenoUtils.isSeedNode()) return;
|
if (HavenoUtils.isSeedNode()) {
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
|
||||||
Thread.dumpStack();
|
|
||||||
}
|
}
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (currentConnection == null) {
|
if (currentConnection == null) {
|
||||||
@ -661,6 +729,10 @@ public final class XmrConnectionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update key image poller
|
||||||
|
keyImagePoller.setDaemon(getDaemon());
|
||||||
|
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
||||||
|
|
||||||
// update polling
|
// update polling
|
||||||
doPollDaemon();
|
doPollDaemon();
|
||||||
if (currentConnection != getConnection()) return; // polling can change connection
|
if (currentConnection != getConnection()) return; // polling can change connection
|
||||||
@ -709,25 +781,31 @@ public final class XmrConnectionService {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
// poll daemon
|
// poll daemon
|
||||||
if (daemon == null) switchToBestConnection();
|
if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
|
||||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
|
||||||
try {
|
try {
|
||||||
|
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||||
lastInfo = daemon.getInfo();
|
lastInfo = daemon.getInfo();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
// skip handling if shutting down
|
// skip handling if shutting down
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
|
|
||||||
// fallback to provided or public nodes if custom connection fails on startup
|
// invoke fallback handling on startup error
|
||||||
if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) {
|
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
|
||||||
if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
if (lastInfo == null && canFallback) {
|
||||||
log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage());
|
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
|
||||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
lastFallbackInvocation = System.currentTimeMillis();
|
||||||
|
if (usedSyncingLocalNodeBeforeStartup) {
|
||||||
|
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
|
||||||
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
|
||||||
|
} else if (isProvidedConnections()) {
|
||||||
|
log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage());
|
||||||
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage());
|
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
|
||||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
|
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
initializeConnections();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,11 +818,13 @@ public final class XmrConnectionService {
|
|||||||
|
|
||||||
// switch to best connection
|
// switch to best connection
|
||||||
switchToBestConnection();
|
switchToBestConnection();
|
||||||
|
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
|
||||||
lastInfo = daemon.getInfo(); // caught internally if still fails
|
lastInfo = daemon.getInfo(); // caught internally if still fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// connected to daemon
|
// connected to daemon
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
|
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
|
||||||
@ -782,16 +862,15 @@ public final class XmrConnectionService {
|
|||||||
downloadListener.progress(percent, blocksLeft, null);
|
downloadListener.progress(percent, blocksLeft, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set peer connections
|
// set available connections
|
||||||
// TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections
|
List<MoneroRpcConnection> availableConnections = new ArrayList<>();
|
||||||
// try {
|
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
||||||
// peers.set(getOnlinePeers());
|
if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) {
|
||||||
// } catch (Exception err) {
|
availableConnections.add(connection);
|
||||||
// // TODO: peers unknown due to restricted RPC call
|
}
|
||||||
// }
|
}
|
||||||
// numPeers.set(peers.get().size());
|
connections.set(availableConnections);
|
||||||
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
|
numConnections.set(availableConnections.size());
|
||||||
peers.set(new ArrayList<MoneroPeer>());
|
|
||||||
|
|
||||||
// notify update
|
// notify update
|
||||||
numUpdates.set(numUpdates.get() + 1);
|
numUpdates.set(numUpdates.get() + 1);
|
||||||
@ -821,13 +900,15 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MoneroPeer> getOnlinePeers() {
|
private boolean isFixedConnection() {
|
||||||
return daemon.getPeers().stream()
|
return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
|
||||||
.filter(peer -> peer.isOnline())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFixedConnection() {
|
private boolean isCustomConnections() {
|
||||||
return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isProvidedConnections() {
|
||||||
|
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils;
|
|||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.xmr.XmrNodeSettings;
|
import haveno.core.xmr.XmrNodeSettings;
|
||||||
import haveno.core.xmr.nodes.XmrNodes;
|
import haveno.core.xmr.nodes.XmrNodes;
|
||||||
|
import haveno.core.xmr.nodes.XmrNodes.XmrNode;
|
||||||
|
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -55,6 +57,7 @@ public class XmrLocalNode {
|
|||||||
private MoneroConnectionManager connectionManager;
|
private MoneroConnectionManager connectionManager;
|
||||||
private final Config config;
|
private final Config config;
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
|
private final XmrNodes xmrNodes;
|
||||||
private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
|
private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
// required arguments
|
// required arguments
|
||||||
@ -69,9 +72,12 @@ public class XmrLocalNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public XmrLocalNode(Config config, Preferences preferences) {
|
public XmrLocalNode(Config config,
|
||||||
|
Preferences preferences,
|
||||||
|
XmrNodes xmrNodes) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
|
this.xmrNodes = xmrNodes;
|
||||||
this.daemon = new MoneroDaemonRpc(getUri());
|
this.daemon = new MoneroDaemonRpc(getUri());
|
||||||
|
|
||||||
// initialize connection manager to listen to local connection
|
// initialize connection manager to listen to local connection
|
||||||
@ -101,7 +107,20 @@ public class XmrLocalNode {
|
|||||||
* Returns whether Haveno should ignore a local Monero node even if it is usable.
|
* Returns whether Haveno should ignore a local Monero node even if it is usable.
|
||||||
*/
|
*/
|
||||||
public boolean shouldBeIgnored() {
|
public boolean shouldBeIgnored() {
|
||||||
return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
if (config.ignoreLocalXmrNode) return true;
|
||||||
|
|
||||||
|
// ignore if fixed connection is not local
|
||||||
|
if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
|
||||||
|
|
||||||
|
// check if local node is within configuration
|
||||||
|
boolean hasConfiguredLocalNode = false;
|
||||||
|
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||||
|
if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
|
||||||
|
hasConfiguredLocalNode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !hasConfiguredLocalNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(XmrLocalNodeListener listener) {
|
public void addListener(XmrLocalNodeListener listener) {
|
||||||
@ -120,7 +139,11 @@ public class XmrLocalNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean equalsUri(String uri) {
|
public boolean equalsUri(String uri) {
|
||||||
|
try {
|
||||||
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
|
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,11 +189,18 @@ public class XmrLocalNode {
|
|||||||
|
|
||||||
var args = new ArrayList<>(MONEROD_ARGS);
|
var args = new ArrayList<>(MONEROD_ARGS);
|
||||||
|
|
||||||
var dataDir = settings.getBlockchainPath();
|
var dataDir = "";
|
||||||
|
if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
|
||||||
|
dataDir = settings.getBlockchainPath();
|
||||||
if (dataDir == null || dataDir.isEmpty()) {
|
if (dataDir == null || dataDir.isEmpty()) {
|
||||||
dataDir = MONEROD_DATADIR;
|
dataDir = MONEROD_DATADIR;
|
||||||
}
|
}
|
||||||
if (dataDir != null) args.add("--data-dir=" + dataDir);
|
} else {
|
||||||
|
dataDir = config.xmrBlockchainPath; // startup config overrides settings
|
||||||
|
}
|
||||||
|
if (dataDir != null && !dataDir.isEmpty()) {
|
||||||
|
args.add("--data-dir=" + dataDir);
|
||||||
|
}
|
||||||
|
|
||||||
var bootstrapUrl = settings.getBootstrapUrl();
|
var bootstrapUrl = settings.getBootstrapUrl();
|
||||||
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
|
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
|
||||||
|
@ -78,6 +78,9 @@ public class OfferInfo implements Payload {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final String splitOutputTxHash;
|
private final String splitOutputTxHash;
|
||||||
private final long splitOutputTxFee;
|
private final long splitOutputTxFee;
|
||||||
|
private final boolean isPrivateOffer;
|
||||||
|
private final String challenge;
|
||||||
|
private final String extraInfo;
|
||||||
|
|
||||||
public OfferInfo(OfferInfoBuilder builder) {
|
public OfferInfo(OfferInfoBuilder builder) {
|
||||||
this.id = builder.getId();
|
this.id = builder.getId();
|
||||||
@ -111,6 +114,9 @@ public class OfferInfo implements Payload {
|
|||||||
this.arbitratorSigner = builder.getArbitratorSigner();
|
this.arbitratorSigner = builder.getArbitratorSigner();
|
||||||
this.splitOutputTxHash = builder.getSplitOutputTxHash();
|
this.splitOutputTxHash = builder.getSplitOutputTxHash();
|
||||||
this.splitOutputTxFee = builder.getSplitOutputTxFee();
|
this.splitOutputTxFee = builder.getSplitOutputTxFee();
|
||||||
|
this.isPrivateOffer = builder.isPrivateOffer();
|
||||||
|
this.challenge = builder.getChallenge();
|
||||||
|
this.extraInfo = builder.getExtraInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OfferInfo toOfferInfo(Offer offer) {
|
public static OfferInfo toOfferInfo(Offer offer) {
|
||||||
@ -137,6 +143,7 @@ public class OfferInfo implements Payload {
|
|||||||
.withIsActivated(isActivated)
|
.withIsActivated(isActivated)
|
||||||
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
|
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
|
||||||
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
|
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
|
||||||
|
.withChallenge(openOffer.getChallenge())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +184,10 @@ public class OfferInfo implements Payload {
|
|||||||
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
|
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
|
||||||
.withVersionNumber(offer.getOfferPayload().getVersionNr())
|
.withVersionNumber(offer.getOfferPayload().getVersionNr())
|
||||||
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
|
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
|
||||||
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
|
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
|
||||||
|
.withIsPrivateOffer(offer.isPrivateOffer())
|
||||||
|
.withChallenge(offer.getChallenge())
|
||||||
|
.withExtraInfo(offer.getCombinedExtraInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -215,9 +225,12 @@ public class OfferInfo implements Payload {
|
|||||||
.setPubKeyRing(pubKeyRing)
|
.setPubKeyRing(pubKeyRing)
|
||||||
.setVersionNr(versionNumber)
|
.setVersionNr(versionNumber)
|
||||||
.setProtocolVersion(protocolVersion)
|
.setProtocolVersion(protocolVersion)
|
||||||
.setSplitOutputTxFee(splitOutputTxFee);
|
.setSplitOutputTxFee(splitOutputTxFee)
|
||||||
|
.setIsPrivateOffer(isPrivateOffer);
|
||||||
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
|
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
|
||||||
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
|
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
|
||||||
|
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
|
||||||
|
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +268,9 @@ public class OfferInfo implements Payload {
|
|||||||
.withArbitratorSigner(proto.getArbitratorSigner())
|
.withArbitratorSigner(proto.getArbitratorSigner())
|
||||||
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
|
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
|
||||||
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
|
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
|
||||||
|
.withIsPrivateOffer(proto.getIsPrivateOffer())
|
||||||
|
.withChallenge(proto.getChallenge())
|
||||||
|
.withExtraInfo(proto.getExtraInfo())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,8 @@ public final class PaymentAccountForm implements PersistablePayload {
|
|||||||
AUSTRALIA_PAYID,
|
AUSTRALIA_PAYID,
|
||||||
CASH_APP,
|
CASH_APP,
|
||||||
PAYPAL,
|
PAYPAL,
|
||||||
VENMO;
|
VENMO,
|
||||||
|
PAYSAFE;
|
||||||
|
|
||||||
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());
|
||||||
|
@ -172,14 +172,14 @@ public class TradeInfo implements Payload {
|
|||||||
.withAmount(trade.getAmount().longValueExact())
|
.withAmount(trade.getAmount().longValueExact())
|
||||||
.withMakerFee(trade.getMakerFee().longValueExact())
|
.withMakerFee(trade.getMakerFee().longValueExact())
|
||||||
.withTakerFee(trade.getTakerFee().longValueExact())
|
.withTakerFee(trade.getTakerFee().longValueExact())
|
||||||
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
|
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
|
||||||
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
|
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
|
||||||
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
|
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
|
||||||
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
|
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
|
||||||
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
|
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
|
||||||
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
|
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
|
||||||
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
|
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
|
||||||
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
|
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
|
||||||
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
|
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
|
||||||
.withPrice(toPreciseTradePrice.apply(trade))
|
.withPrice(toPreciseTradePrice.apply(trade))
|
||||||
.withVolume(toRoundedVolume.apply(trade))
|
.withVolume(toRoundedVolume.apply(trade))
|
||||||
|
@ -63,6 +63,9 @@ public final class OfferInfoBuilder {
|
|||||||
private String arbitratorSigner;
|
private String arbitratorSigner;
|
||||||
private String splitOutputTxHash;
|
private String splitOutputTxHash;
|
||||||
private long splitOutputTxFee;
|
private long splitOutputTxFee;
|
||||||
|
private boolean isPrivateOffer;
|
||||||
|
private String challenge;
|
||||||
|
private String extraInfo;
|
||||||
|
|
||||||
public OfferInfoBuilder withId(String id) {
|
public OfferInfoBuilder withId(String id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -234,6 +237,21 @@ public final class OfferInfoBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) {
|
||||||
|
this.isPrivateOffer = isPrivateOffer;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OfferInfoBuilder withChallenge(String challenge) {
|
||||||
|
this.challenge = challenge;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OfferInfoBuilder withExtraInfo(String extraInfo) {
|
||||||
|
this.extraInfo = extraInfo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public OfferInfo build() {
|
public OfferInfo build() {
|
||||||
return new OfferInfo(this);
|
return new OfferInfo(this);
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ public class AppStartupState {
|
|||||||
isWalletSynced.set(true);
|
isWalletSynced.set(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if (xmrConnectionService.hasSufficientPeersForBroadcast())
|
if (xmrConnectionService.hasSufficientPeersForBroadcast())
|
||||||
hasSufficientPeersForBroadcast.set(true);
|
hasSufficientPeersForBroadcast.set(true);
|
||||||
});
|
});
|
||||||
|
@ -178,6 +178,9 @@ public class DomainInitialisation {
|
|||||||
closedTradableManager.onAllServicesInitialized();
|
closedTradableManager.onAllServicesInitialized();
|
||||||
failedTradesManager.onAllServicesInitialized();
|
failedTradesManager.onAllServicesInitialized();
|
||||||
|
|
||||||
|
filterManager.setFilterWarningHandler(filterWarningHandler);
|
||||||
|
filterManager.onAllServicesInitialized();
|
||||||
|
|
||||||
openOfferManager.onAllServicesInitialized();
|
openOfferManager.onAllServicesInitialized();
|
||||||
|
|
||||||
balances.onAllServicesInitialized();
|
balances.onAllServicesInitialized();
|
||||||
@ -199,10 +202,6 @@ public class DomainInitialisation {
|
|||||||
priceFeedService.setCurrencyCodeOnInit();
|
priceFeedService.setCurrencyCodeOnInit();
|
||||||
priceFeedService.startRequestingPrices();
|
priceFeedService.startRequestingPrices();
|
||||||
|
|
||||||
filterManager.setFilterWarningHandler(filterWarningHandler);
|
|
||||||
filterManager.onAllServicesInitialized();
|
|
||||||
|
|
||||||
|
|
||||||
mobileNotificationService.onAllServicesInitialized();
|
mobileNotificationService.onAllServicesInitialized();
|
||||||
myOfferTakenEvents.onAllServicesInitialized();
|
myOfferTakenEvents.onAllServicesInitialized();
|
||||||
tradeEvents.onAllServicesInitialized();
|
tradeEvents.onAllServicesInitialized();
|
||||||
|
@ -100,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
protected AppModule module;
|
protected AppModule module;
|
||||||
protected Config config;
|
protected Config config;
|
||||||
@Getter
|
@Getter
|
||||||
protected boolean isShutdownInProgress;
|
protected boolean isShutDownStarted;
|
||||||
private boolean isReadOnly;
|
private boolean isReadOnly;
|
||||||
private Thread keepRunningThread;
|
private Thread keepRunningThread;
|
||||||
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
|
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
|
||||||
@ -330,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
|
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
|
||||||
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||||
|
|
||||||
// ignore if shut down in progress
|
// ignore if shut down started
|
||||||
if (isShutdownInProgress) {
|
if (isShutDownStarted) {
|
||||||
log.info("Ignoring call to gracefulShutDown, already in progress");
|
log.info("Ignoring call to gracefulShutDown, already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isShutdownInProgress = true;
|
isShutDownStarted = true;
|
||||||
|
|
||||||
ResultHandler resultHandler;
|
ResultHandler resultHandler;
|
||||||
if (shutdownCompletedHandler != null) {
|
if (shutdownCompletedHandler != null) {
|
||||||
@ -357,46 +357,47 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
|
|
||||||
// notify trade protocols and wallets to prepare for shut down before shutting down
|
// notify trade protocols and wallets to prepare for shut down before shutting down
|
||||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
|
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
|
||||||
try {
|
try {
|
||||||
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
|
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e);
|
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
injector.getInstance(TradeManager.class).shutDown();
|
|
||||||
injector.getInstance(PriceFeedService.class).shutDown();
|
injector.getInstance(PriceFeedService.class).shutDown();
|
||||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||||
|
|
||||||
// shut down open offer manager
|
// shut down open offer manager
|
||||||
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
log.info("Shutting down OpenOfferManager");
|
||||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||||
|
|
||||||
// shut down offer book service
|
// listen for shut down of wallets setup
|
||||||
injector.getInstance(OfferBookService.class).shutDown();
|
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||||
|
|
||||||
// shut down p2p service
|
// shut down p2p service
|
||||||
|
log.info("Shutting down P2P service");
|
||||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||||
|
|
||||||
// shut down monero wallets and connections
|
|
||||||
log.info("Shutting down wallet and connection services");
|
|
||||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
|
||||||
|
|
||||||
// done shutting down
|
// done shutting down
|
||||||
log.info("Graceful shutdown completed. Exiting now.");
|
log.info("Graceful shutdown completed. Exiting now.");
|
||||||
module.close(injector);
|
module.close(injector);
|
||||||
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
|
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// shut down trade and wallet services
|
||||||
|
log.info("Shutting down trade and wallet services");
|
||||||
|
injector.getInstance(OfferBookService.class).shutDown();
|
||||||
|
injector.getInstance(TradeManager.class).shutDown();
|
||||||
injector.getInstance(BtcWalletService.class).shutDown();
|
injector.getInstance(BtcWalletService.class).shutDown();
|
||||||
injector.getInstance(XmrWalletService.class).shutDown();
|
injector.getInstance(XmrWalletService.class).shutDown();
|
||||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||||
injector.getInstance(WalletsSetup.class).shutDown();
|
injector.getInstance(WalletsSetup.class).shutDown();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
|
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
|
||||||
completeShutdown(resultHandler, EXIT_FAILURE, systemExit);
|
completeShutdown(resultHandler, EXIT_FAILURE, systemExit);
|
||||||
|
@ -75,6 +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.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));
|
||||||
@ -85,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
|||||||
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
|
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
|
||||||
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
|
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
|
||||||
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
|
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
|
||||||
havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
|
havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler"));
|
||||||
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
|
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
|
||||||
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
|
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
|
||||||
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
|
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
|
||||||
|
@ -55,6 +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.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;
|
||||||
@ -158,6 +159,9 @@ public class HavenoSetup {
|
|||||||
rejectedTxErrorMessageHandler;
|
rejectedTxErrorMessageHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
|
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
|
||||||
|
@Setter
|
||||||
|
@Nullable
|
||||||
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -173,7 +177,7 @@ public class HavenoSetup {
|
|||||||
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
|
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
private Runnable showPopupIfInvalidBtcConfigHandler;
|
private Runnable showPopupIfInvalidXmrConfigHandler;
|
||||||
@Setter
|
@Setter
|
||||||
@Nullable
|
@Nullable
|
||||||
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
|
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
|
||||||
@ -366,7 +370,7 @@ public class HavenoSetup {
|
|||||||
// install monerod
|
// install monerod
|
||||||
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
|
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
|
||||||
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
|
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
|
||||||
if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) {
|
if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) {
|
||||||
log.info("Installing monerod");
|
log.info("Installing monerod");
|
||||||
monerodFile.getParentFile().mkdirs();
|
monerodFile.getParentFile().mkdirs();
|
||||||
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
|
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
|
||||||
@ -376,7 +380,7 @@ public class HavenoSetup {
|
|||||||
// install monero-wallet-rpc
|
// install monero-wallet-rpc
|
||||||
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
|
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
|
||||||
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
|
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
|
||||||
if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) {
|
if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) {
|
||||||
log.info("Installing monero-wallet-rpc");
|
log.info("Installing monero-wallet-rpc");
|
||||||
moneroWalletRpcFile.getParentFile().mkdirs();
|
moneroWalletRpcFile.getParentFile().mkdirs();
|
||||||
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
|
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
|
||||||
@ -426,6 +430,12 @@ public class HavenoSetup {
|
|||||||
getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||||
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||||
|
|
||||||
|
// listen for fallback handling
|
||||||
|
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (displayMoneroConnectionFallbackHandler == null) return;
|
||||||
|
displayMoneroConnectionFallbackHandler.accept(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
log.info("Init P2P network");
|
log.info("Init P2P network");
|
||||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
|
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
|
||||||
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
|
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
|
||||||
@ -452,7 +462,7 @@ public class HavenoSetup {
|
|||||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
|
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
|
||||||
walletAppSetup.init(chainFileLockedExceptionHandler,
|
walletAppSetup.init(chainFileLockedExceptionHandler,
|
||||||
showFirstPopupIfResyncSPVRequestedHandler,
|
showFirstPopupIfResyncSPVRequestedHandler,
|
||||||
showPopupIfInvalidBtcConfigHandler,
|
showPopupIfInvalidXmrConfigHandler,
|
||||||
() -> {},
|
() -> {},
|
||||||
() -> {});
|
() -> {});
|
||||||
}
|
}
|
||||||
@ -725,6 +735,10 @@ public class HavenoSetup {
|
|||||||
return xmrConnectionService.getConnectionServiceErrorMsg();
|
return xmrConnectionService.getConnectionServiceErrorMsg();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
|
||||||
|
return xmrConnectionService.getConnectionServiceFallbackType();
|
||||||
|
}
|
||||||
|
|
||||||
public StringProperty getTopErrorMsg() {
|
public StringProperty getTopErrorMsg() {
|
||||||
return topErrorMsg;
|
return topErrorMsg;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ public class P2PNetworkSetup {
|
|||||||
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
|
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
|
||||||
|
|
||||||
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
|
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
|
||||||
xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||||
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
|
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
|
||||||
String result;
|
String result;
|
||||||
int p2pPeers = (int) numP2pPeers;
|
int p2pPeers = (int) numP2pPeers;
|
||||||
|
@ -117,10 +117,10 @@ public class WalletAppSetup {
|
|||||||
|
|
||||||
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
|
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
|
||||||
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
|
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
|
||||||
@Nullable Runnable showPopupIfInvalidBtcConfigHandler,
|
@Nullable Runnable showPopupIfInvalidXmrConfigHandler,
|
||||||
Runnable downloadCompleteHandler,
|
Runnable downloadCompleteHandler,
|
||||||
Runnable walletInitializedHandler) {
|
Runnable walletInitializedHandler) {
|
||||||
log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion());
|
log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
|
||||||
|
|
||||||
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
|
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
|
||||||
xmrInfoBinding = EasyBind.combine(
|
xmrInfoBinding = EasyBind.combine(
|
||||||
@ -199,8 +199,8 @@ public class WalletAppSetup {
|
|||||||
walletInitializedHandler.run();
|
walletInitializedHandler.run();
|
||||||
},
|
},
|
||||||
exception -> {
|
exception -> {
|
||||||
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
|
if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
|
||||||
showPopupIfInvalidBtcConfigHandler.run();
|
showPopupIfInvalidXmrConfigHandler.run();
|
||||||
} else {
|
} else {
|
||||||
walletServiceException.set(exception);
|
walletServiceException.set(exception);
|
||||||
}
|
}
|
||||||
|
@ -105,21 +105,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
|||||||
public void gracefulShutDown(ResultHandler resultHandler) {
|
public void gracefulShutDown(ResultHandler resultHandler) {
|
||||||
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||||
|
|
||||||
// ignore if shut down in progress
|
// ignore if shut down started
|
||||||
if (isShutdownInProgress) {
|
if (isShutDownStarted) {
|
||||||
log.info("Ignoring call to gracefulShutDown, already in progress");
|
log.info("Ignoring call to gracefulShutDown, already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isShutdownInProgress = true;
|
isShutDownStarted = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (injector != null) {
|
if (injector != null) {
|
||||||
|
|
||||||
// notify trade protocols and wallets to prepare for shut down
|
// notify trade protocols and wallets to prepare for shut down
|
||||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
|
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
|
||||||
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
|
|
||||||
try {
|
try {
|
||||||
ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout
|
ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -127,25 +127,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonFileManager.shutDownAllInstances();
|
JsonFileManager.shutDownAllInstances();
|
||||||
injector.getInstance(TradeManager.class).shutDown();
|
|
||||||
injector.getInstance(PriceFeedService.class).shutDown();
|
injector.getInstance(PriceFeedService.class).shutDown();
|
||||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||||
|
|
||||||
// shut down open offer manager
|
// shut down open offer manager
|
||||||
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
log.info("Shutting down OpenOfferManager");
|
||||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||||
|
|
||||||
// shut down offer book service
|
// listen for shut down of wallets setup
|
||||||
injector.getInstance(OfferBookService.class).shutDown();
|
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||||
|
|
||||||
// shut down p2p service
|
// shut down p2p service
|
||||||
|
log.info("Shutting down P2P service");
|
||||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||||
|
|
||||||
// shut down monero wallets and connections
|
|
||||||
log.info("Shutting down wallet and connection services");
|
|
||||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
|
||||||
module.close(injector);
|
module.close(injector);
|
||||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||||
|
|
||||||
@ -155,6 +151,11 @@ 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
|
||||||
|
log.info("Shutting down trade and wallet services");
|
||||||
|
injector.getInstance(OfferBookService.class).shutDown();
|
||||||
|
injector.getInstance(TradeManager.class).shutDown();
|
||||||
injector.getInstance(BtcWalletService.class).shutDown();
|
injector.getInstance(BtcWalletService.class).shutDown();
|
||||||
injector.getInstance(XmrWalletService.class).shutDown();
|
injector.getInstance(XmrWalletService.class).shutDown();
|
||||||
injector.getInstance(XmrConnectionService.class).shutDown();
|
injector.getInstance(XmrConnectionService.class).shutDown();
|
||||||
|
@ -406,6 +406,10 @@ public class FilterManager {
|
|||||||
.anyMatch(e -> e.equals(address));
|
.anyMatch(e -> e.equals(address));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisableTradeBelowVersion() {
|
||||||
|
return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean requireUpdateToNewVersionForTrading() {
|
public boolean requireUpdateToNewVersionForTrading() {
|
||||||
if (getFilter() == null) {
|
if (getFilter() == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -200,7 +200,10 @@ public class CurrencyUtil {
|
|||||||
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
|
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
|
||||||
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("USDT-ERC20", "Tether USD (ERC20)"));
|
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
|
||||||
|
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)"));
|
||||||
|
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)"));
|
||||||
result.sort(TradeCurrency::compareTo);
|
result.sort(TradeCurrency::compareTo);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -296,7 +299,7 @@ public class CurrencyUtil {
|
|||||||
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
|
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
|
||||||
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
|
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
|
||||||
}
|
}
|
||||||
if (isCryptoCurrencyBase(currencyCode)) {
|
if (isCryptoCurrencyCodeBase(currencyCode)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,16 +328,18 @@ public class CurrencyUtil {
|
|||||||
return isCryptoCurrency;
|
return isCryptoCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCryptoCurrencyBase(String currencyCode) {
|
private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
|
||||||
if (currencyCode == null) return false;
|
if (currencyCode == null) return false;
|
||||||
currencyCode = currencyCode.toUpperCase();
|
currencyCode = currencyCode.toUpperCase();
|
||||||
return currencyCode.equals("USDT");
|
return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrencyCodeBase(String currencyCode) {
|
public static String getCurrencyCodeBase(String currencyCode) {
|
||||||
if (currencyCode == null) return null;
|
if (currencyCode == null) return null;
|
||||||
currencyCode = currencyCode.toUpperCase();
|
currencyCode = currencyCode.toUpperCase();
|
||||||
if (currencyCode.contains("USDT")) return "USDT";
|
if (currencyCode.contains("USDT")) return "USDT";
|
||||||
|
if (currencyCode.contains("USDC")) return "USDC";
|
||||||
|
if (currencyCode.contains("DAI")) return "DAI";
|
||||||
return currencyCode;
|
return currencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService;
|
|||||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.user.Preferences;
|
|
||||||
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;
|
||||||
@ -94,6 +92,7 @@ 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,
|
||||||
@ -102,10 +101,12 @@ public class CreateOfferService {
|
|||||||
Price fixedPrice,
|
Price fixedPrice,
|
||||||
boolean useMarketBasedPrice,
|
boolean useMarketBasedPrice,
|
||||||
double marketPriceMargin,
|
double marketPriceMargin,
|
||||||
double securityDepositAsDouble,
|
double securityDepositPct,
|
||||||
PaymentAccount paymentAccount) {
|
PaymentAccount paymentAccount,
|
||||||
|
boolean isPrivateOffer,
|
||||||
log.info("create and get offer with offerId={}, " +
|
boolean buyerAsTakerWithoutDeposit,
|
||||||
|
String extraInfo) {
|
||||||
|
log.info("Create and get offer with offerId={}, " +
|
||||||
"currencyCode={}, " +
|
"currencyCode={}, " +
|
||||||
"direction={}, " +
|
"direction={}, " +
|
||||||
"fixedPrice={}, " +
|
"fixedPrice={}, " +
|
||||||
@ -113,7 +114,10 @@ public class CreateOfferService {
|
|||||||
"marketPriceMargin={}, " +
|
"marketPriceMargin={}, " +
|
||||||
"amount={}, " +
|
"amount={}, " +
|
||||||
"minAmount={}, " +
|
"minAmount={}, " +
|
||||||
"securityDeposit={}",
|
"securityDepositPct={}, " +
|
||||||
|
"isPrivateOffer={}, " +
|
||||||
|
"buyerAsTakerWithoutDeposit={}, " +
|
||||||
|
"extraInfo={}",
|
||||||
offerId,
|
offerId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
direction,
|
direction,
|
||||||
@ -122,7 +126,19 @@ public class CreateOfferService {
|
|||||||
marketPriceMargin,
|
marketPriceMargin,
|
||||||
amount,
|
amount,
|
||||||
minAmount,
|
minAmount,
|
||||||
securityDepositAsDouble);
|
securityDepositPct,
|
||||||
|
isPrivateOffer,
|
||||||
|
buyerAsTakerWithoutDeposit,
|
||||||
|
extraInfo);
|
||||||
|
|
||||||
|
// must nullify empty string so contracts match
|
||||||
|
if ("".equals(extraInfo)) extraInfo = null;
|
||||||
|
|
||||||
|
// verify buyer as taker security deposit
|
||||||
|
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
|
||||||
|
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
|
||||||
|
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
|
||||||
|
}
|
||||||
|
|
||||||
// verify fixed price xor market price with margin
|
// verify fixed price xor market price with margin
|
||||||
if (fixedPrice != null) {
|
if (fixedPrice != null) {
|
||||||
@ -130,25 +146,29 @@ public class CreateOfferService {
|
|||||||
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
|
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
|
||||||
}
|
}
|
||||||
|
|
||||||
long creationTime = new Date().getTime();
|
// verify price
|
||||||
NodeAddress makerAddress = p2PService.getAddress();
|
|
||||||
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
||||||
useMarketBasedPrice &&
|
useMarketBasedPrice &&
|
||||||
isMarketPriceAvailable(currencyCode) &&
|
isMarketPriceAvailable(currencyCode) &&
|
||||||
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
||||||
|
|
||||||
// verify price
|
|
||||||
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 for fixed-price offer
|
// adjust amount and min amount
|
||||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
|
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||||
if (fixedPrice != null) {
|
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
|
||||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
// generate one-time challenge for private offer
|
||||||
|
String challenge = null;
|
||||||
|
String challengeHash = null;
|
||||||
|
if (isPrivateOffer) {
|
||||||
|
challenge = HavenoUtils.generateChallenge();
|
||||||
|
challengeHash = HavenoUtils.getChallengeHash(challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long creationTime = new Date().getTime();
|
||||||
|
NodeAddress makerAddress = p2PService.getAddress();
|
||||||
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
|
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
|
||||||
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
||||||
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
||||||
@ -161,21 +181,16 @@ public class CreateOfferService {
|
|||||||
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;
|
||||||
// reserved for future use cases
|
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
|
||||||
// Use null values if not set
|
|
||||||
boolean isPrivateOffer = false;
|
|
||||||
boolean useAutoClose = false;
|
boolean useAutoClose = false;
|
||||||
boolean useReOpenAfterAutoClose = false;
|
boolean useReOpenAfterAutoClose = false;
|
||||||
long lowerClosePrice = 0;
|
long lowerClosePrice = 0;
|
||||||
long upperClosePrice = 0;
|
long upperClosePrice = 0;
|
||||||
String hashOfChallenge = null;
|
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
|
||||||
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
|
|
||||||
currencyCode,
|
|
||||||
direction);
|
|
||||||
|
|
||||||
offerUtil.validateOfferData(
|
offerUtil.validateOfferData(
|
||||||
securityDepositAsDouble,
|
securityDepositPct,
|
||||||
paymentAccount,
|
paymentAccount,
|
||||||
currencyCode);
|
currencyCode);
|
||||||
|
|
||||||
@ -189,11 +204,11 @@ public class CreateOfferService {
|
|||||||
useMarketBasedPriceValue,
|
useMarketBasedPriceValue,
|
||||||
amountAsLong,
|
amountAsLong,
|
||||||
minAmountAsLong,
|
minAmountAsLong,
|
||||||
HavenoUtils.MAKER_FEE_PCT,
|
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
|
||||||
HavenoUtils.TAKER_FEE_PCT,
|
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
|
||||||
HavenoUtils.PENALTY_FEE_PCT,
|
HavenoUtils.PENALTY_FEE_PCT,
|
||||||
securityDepositAsDouble,
|
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
|
||||||
securityDepositAsDouble,
|
securityDepositPct,
|
||||||
baseCurrencyCode,
|
baseCurrencyCode,
|
||||||
counterCurrencyCode,
|
counterCurrencyCode,
|
||||||
paymentAccount.getPaymentMethod().getId(),
|
paymentAccount.getPaymentMethod().getId(),
|
||||||
@ -211,44 +226,110 @@ public class CreateOfferService {
|
|||||||
upperClosePrice,
|
upperClosePrice,
|
||||||
lowerClosePrice,
|
lowerClosePrice,
|
||||||
isPrivateOffer,
|
isPrivateOffer,
|
||||||
hashOfChallenge,
|
challengeHash,
|
||||||
extraDataMap,
|
extraDataMap,
|
||||||
Version.TRADE_PROTOCOL_VERSION,
|
Version.TRADE_PROTOCOL_VERSION,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null);
|
null,
|
||||||
|
extraInfo);
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
|
offer.setChallenge(challenge);
|
||||||
return offer;
|
return offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigInteger getReservedFundsForOffer(OfferDirection direction,
|
// TODO: add trigger price?
|
||||||
BigInteger amount,
|
public Offer createClonedOffer(Offer sourceOffer,
|
||||||
double buyerSecurityDeposit,
|
String currencyCode,
|
||||||
double sellerSecurityDeposit) {
|
Price fixedPrice,
|
||||||
|
boolean useMarketBasedPrice,
|
||||||
|
double marketPriceMargin,
|
||||||
|
PaymentAccount paymentAccount,
|
||||||
|
String extraInfo) {
|
||||||
|
log.info("Cloning offer with sourceId={}, " +
|
||||||
|
"currencyCode={}, " +
|
||||||
|
"fixedPrice={}, " +
|
||||||
|
"useMarketBasedPrice={}, " +
|
||||||
|
"marketPriceMargin={}, " +
|
||||||
|
"extraInfo={}",
|
||||||
|
sourceOffer.getId(),
|
||||||
|
currencyCode,
|
||||||
|
fixedPrice == null ? null : fixedPrice.getValue(),
|
||||||
|
useMarketBasedPrice,
|
||||||
|
marketPriceMargin,
|
||||||
|
extraInfo);
|
||||||
|
|
||||||
BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
|
OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
|
||||||
amount,
|
String newOfferId = OfferUtil.getRandomOfferId();
|
||||||
buyerSecurityDeposit,
|
Offer editedOffer = createAndGetOffer(newOfferId,
|
||||||
sellerSecurityDeposit);
|
sourceOfferPayload.getDirection(),
|
||||||
if (!offerUtil.isBuyOffer(direction))
|
currencyCode,
|
||||||
reservedFundsForOffer = reservedFundsForOffer.add(amount);
|
BigInteger.valueOf(sourceOfferPayload.getAmount()),
|
||||||
|
BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
|
||||||
|
fixedPrice,
|
||||||
|
useMarketBasedPrice,
|
||||||
|
marketPriceMargin,
|
||||||
|
sourceOfferPayload.getSellerSecurityDepositPct(),
|
||||||
|
paymentAccount,
|
||||||
|
sourceOfferPayload.isPrivateOffer(),
|
||||||
|
sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
|
||||||
|
extraInfo);
|
||||||
|
|
||||||
return reservedFundsForOffer;
|
// generate one-time challenge for private offer
|
||||||
|
String challenge = null;
|
||||||
|
String challengeHash = null;
|
||||||
|
if (sourceOfferPayload.isPrivateOffer()) {
|
||||||
|
challenge = HavenoUtils.generateChallenge();
|
||||||
|
challengeHash = HavenoUtils.getChallengeHash(challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigInteger getSecurityDeposit(OfferDirection direction,
|
OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
|
||||||
BigInteger amount,
|
long date = new Date().getTime();
|
||||||
double buyerSecurityDeposit,
|
OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
|
||||||
double sellerSecurityDeposit) {
|
date,
|
||||||
return offerUtil.isBuyOffer(direction) ?
|
sourceOfferPayload.getOwnerNodeAddress(),
|
||||||
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
|
sourceOfferPayload.getPubKeyRing(),
|
||||||
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
|
sourceOfferPayload.getDirection(),
|
||||||
}
|
editedOfferPayload.getPrice(),
|
||||||
|
editedOfferPayload.getMarketPriceMarginPct(),
|
||||||
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
|
editedOfferPayload.isUseMarketBasedPrice(),
|
||||||
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
|
sourceOfferPayload.getAmount(),
|
||||||
Restrictions.getSellerSecurityDepositAsPercent();
|
sourceOfferPayload.getMinAmount(),
|
||||||
|
sourceOfferPayload.getMakerFeePct(),
|
||||||
|
sourceOfferPayload.getTakerFeePct(),
|
||||||
|
sourceOfferPayload.getPenaltyFeePct(),
|
||||||
|
sourceOfferPayload.getBuyerSecurityDepositPct(),
|
||||||
|
sourceOfferPayload.getSellerSecurityDepositPct(),
|
||||||
|
editedOfferPayload.getBaseCurrencyCode(),
|
||||||
|
editedOfferPayload.getCounterCurrencyCode(),
|
||||||
|
editedOfferPayload.getPaymentMethodId(),
|
||||||
|
editedOfferPayload.getMakerPaymentAccountId(),
|
||||||
|
editedOfferPayload.getCountryCode(),
|
||||||
|
editedOfferPayload.getAcceptedCountryCodes(),
|
||||||
|
editedOfferPayload.getBankId(),
|
||||||
|
editedOfferPayload.getAcceptedBankIds(),
|
||||||
|
editedOfferPayload.getVersionNr(),
|
||||||
|
sourceOfferPayload.getBlockHeightAtOfferCreation(),
|
||||||
|
editedOfferPayload.getMaxTradeLimit(),
|
||||||
|
editedOfferPayload.getMaxTradePeriod(),
|
||||||
|
sourceOfferPayload.isUseAutoClose(),
|
||||||
|
sourceOfferPayload.isUseReOpenAfterAutoClose(),
|
||||||
|
sourceOfferPayload.getLowerClosePrice(),
|
||||||
|
sourceOfferPayload.getUpperClosePrice(),
|
||||||
|
sourceOfferPayload.isPrivateOffer(),
|
||||||
|
challengeHash,
|
||||||
|
editedOfferPayload.getExtraDataMap(),
|
||||||
|
sourceOfferPayload.getProtocolVersion(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
sourceOfferPayload.getReserveTxKeyImages(),
|
||||||
|
editedOfferPayload.getExtraInfo());
|
||||||
|
Offer clonedOffer = new Offer(clonedOfferPayload);
|
||||||
|
clonedOffer.setPriceFeedService(priceFeedService);
|
||||||
|
clonedOffer.setChallenge(challenge);
|
||||||
|
clonedOffer.setState(Offer.State.AVAILABLE);
|
||||||
|
return clonedOffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -259,26 +340,4 @@ public class CreateOfferService {
|
|||||||
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
|
||||||
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
|
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) {
|
|
||||||
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount);
|
|
||||||
return getBoundedBuyerSecurityDeposit(percentOfAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) {
|
|
||||||
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount);
|
|
||||||
return getBoundedSellerSecurityDeposit(percentOfAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) {
|
|
||||||
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
|
|
||||||
// MinBuyerSecurityDeposit from Restrictions.
|
|
||||||
return Restrictions.getMinBuyerSecurityDeposit().max(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) {
|
|
||||||
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
|
|
||||||
// MinSellerSecurityDeposit from Restrictions.
|
|
||||||
return Restrictions.getMinSellerSecurityDeposit().max(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
package haveno.core.offer;
|
package haveno.core.offer;
|
||||||
|
|
||||||
import haveno.common.ThreadUtils;
|
import haveno.common.ThreadUtils;
|
||||||
import haveno.common.UserThread;
|
|
||||||
import haveno.common.crypto.KeyRing;
|
import haveno.common.crypto.KeyRing;
|
||||||
import haveno.common.crypto.PubKeyRing;
|
import haveno.common.crypto.PubKeyRing;
|
||||||
import haveno.common.handlers.ErrorMessageHandler;
|
import haveno.common.handlers.ErrorMessageHandler;
|
||||||
@ -115,6 +114,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
|||||||
@Setter
|
@Setter
|
||||||
transient private boolean isReservedFundsSpent;
|
transient private boolean isReservedFundsSpent;
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Nullable
|
||||||
|
transient private String challenge;
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
@ -275,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setErrorMessage(String errorMessage) {
|
public void setErrorMessage(String errorMessage) {
|
||||||
UserThread.await(() -> errorMessageProperty.set(errorMessage));
|
errorMessageProperty.set(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -337,6 +342,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
|||||||
return offerPayload.getSellerSecurityDepositPct();
|
return offerPayload.getSellerSecurityDepositPct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPrivateOffer() {
|
||||||
|
return offerPayload.isPrivateOffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChallengeHash() {
|
||||||
|
return offerPayload.getChallengeHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBuyerAsTakerWithoutDeposit() {
|
||||||
|
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
public BigInteger getMaxTradeLimit() {
|
public BigInteger getMaxTradeLimit() {
|
||||||
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
|
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
|
||||||
}
|
}
|
||||||
@ -403,7 +420,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExtraInfo() {
|
public String getCombinedExtraInfo() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) {
|
||||||
|
sb.append(getOfferExtraInfo());
|
||||||
|
}
|
||||||
|
if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) {
|
||||||
|
if (sb.length() > 0) sb.append("\n\n");
|
||||||
|
sb.append(getPaymentAccountExtraInfo());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOfferExtraInfo() {
|
||||||
|
return offerPayload.getExtraInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaymentAccountExtraInfo() {
|
||||||
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
|
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
|
||||||
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
|
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
|
||||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
|
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
|
||||||
@ -414,6 +447,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
|||||||
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
|
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
|
||||||
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
|
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
|
||||||
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
|
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
|
||||||
|
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO))
|
||||||
|
return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO);
|
||||||
else
|
else
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,9 @@ package haveno.core.offer;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
|
|
||||||
|
import haveno.common.ThreadUtils;
|
||||||
|
import haveno.common.Timer;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.config.Config;
|
import haveno.common.config.Config;
|
||||||
import haveno.common.file.JsonFileManager;
|
import haveno.common.file.JsonFileManager;
|
||||||
@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService;
|
|||||||
import haveno.core.filter.FilterManager;
|
import haveno.core.filter.FilterManager;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.provider.price.PriceFeedService;
|
import haveno.core.provider.price.PriceFeedService;
|
||||||
import haveno.core.trade.HavenoUtils;
|
|
||||||
import haveno.core.util.JsonUtil;
|
import haveno.core.util.JsonUtil;
|
||||||
|
import haveno.core.xmr.wallet.Restrictions;
|
||||||
import haveno.core.xmr.wallet.XmrKeyImageListener;
|
import haveno.core.xmr.wallet.XmrKeyImageListener;
|
||||||
import haveno.core.xmr.wallet.XmrKeyImagePoller;
|
|
||||||
import haveno.network.p2p.BootstrapListener;
|
import haveno.network.p2p.BootstrapListener;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import haveno.network.p2p.storage.HashMapChangedListener;
|
import haveno.network.p2p.storage.HashMapChangedListener;
|
||||||
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
|
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||||
|
import haveno.network.utils.Utils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles storage and retrieval of offers.
|
* Handles validation and announcement of offers added or removed.
|
||||||
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
|
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class OfferBookService {
|
public class OfferBookService {
|
||||||
|
|
||||||
|
private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
private final P2PService p2PService;
|
private final P2PService p2PService;
|
||||||
private final PriceFeedService priceFeedService;
|
private final PriceFeedService priceFeedService;
|
||||||
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
|
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
|
||||||
private final FilterManager filterManager;
|
private final FilterManager filterManager;
|
||||||
private final JsonFileManager jsonFileManager;
|
private final JsonFileManager jsonFileManager;
|
||||||
private final XmrConnectionService xmrConnectionService;
|
private final XmrConnectionService xmrConnectionService;
|
||||||
|
private final List<Offer> validOffers = new ArrayList<Offer>();
|
||||||
// poll key images of offers
|
private final List<Offer> invalidOffers = new ArrayList<Offer>();
|
||||||
private XmrKeyImagePoller keyImagePoller;
|
private final Map<String, Timer> invalidOfferTimers = new HashMap<>();
|
||||||
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
|
|
||||||
|
|
||||||
public interface OfferBookChangedListener {
|
public interface OfferBookChangedListener {
|
||||||
void onAdded(Offer offer);
|
void onAdded(Offer offer);
|
||||||
|
|
||||||
void onRemoved(Offer offer);
|
void onRemoved(Offer offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,51 +113,59 @@ public class OfferBookService {
|
|||||||
this.xmrConnectionService = xmrConnectionService;
|
this.xmrConnectionService = xmrConnectionService;
|
||||||
jsonFileManager = new JsonFileManager(storageDir);
|
jsonFileManager = new JsonFileManager(storageDir);
|
||||||
|
|
||||||
// listen for connection changes to monerod
|
|
||||||
xmrConnectionService.addConnectionListener((connection) -> {
|
|
||||||
maybeInitializeKeyImagePoller();
|
|
||||||
keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
|
|
||||||
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen for offers
|
// listen for offers
|
||||||
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||||
UserThread.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
maybeInitializeKeyImagePoller();
|
|
||||||
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
|
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
setReservedFundsSpent(offer);
|
synchronized (validOffers) {
|
||||||
synchronized (offerBookChangedListeners) {
|
try {
|
||||||
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
|
validateOfferPayload(offerPayload);
|
||||||
|
replaceValidOffer(offer);
|
||||||
|
announceOfferAdded(offer);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// ignore illegal offers
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
replaceInvalidOffer(offer); // offer can become valid later
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, OfferBookService.class.getSimpleName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||||
UserThread.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
maybeInitializeKeyImagePoller();
|
removeValidOffer(offerPayload.getId());
|
||||||
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
|
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
setReservedFundsSpent(offer);
|
announceOfferRemoved(offer);
|
||||||
synchronized (offerBookChangedListeners) {
|
|
||||||
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
|
// check if invalid offers are now valid
|
||||||
|
synchronized (invalidOffers) {
|
||||||
|
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
|
||||||
|
try {
|
||||||
|
validateOfferPayload(invalidOffer.getOfferPayload());
|
||||||
|
removeInvalidOffer(invalidOffer.getId());
|
||||||
|
replaceValidOffer(invalidOffer);
|
||||||
|
announceOfferAdded(invalidOffer);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, OfferBookService.class.getSimpleName());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,6 +188,16 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listen for changes to key images
|
||||||
|
xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() {
|
||||||
|
@Override
|
||||||
|
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
|
||||||
|
for (String keyImage : spentStatuses.keySet()) {
|
||||||
|
updateAffectedOffers(keyImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -178,6 +205,10 @@ public class OfferBookService {
|
|||||||
// API
|
// API
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public boolean hasOffer(String offerId) {
|
||||||
|
return hasValidOffer(offerId);
|
||||||
|
}
|
||||||
|
|
||||||
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
if (filterManager.requireUpdateToNewVersionForTrading()) {
|
if (filterManager.requireUpdateToNewVersionForTrading()) {
|
||||||
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
|
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
|
||||||
@ -233,16 +264,9 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Offer> getOffers() {
|
public List<Offer> getOffers() {
|
||||||
return p2PService.getDataMap().values().stream()
|
synchronized (validOffers) {
|
||||||
.filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload)
|
return new ArrayList<>(validOffers);
|
||||||
.map(data -> {
|
}
|
||||||
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
|
|
||||||
Offer offer = new Offer(offerPayload);
|
|
||||||
offer.setPriceFeedService(priceFeedService);
|
|
||||||
setReservedFundsSpent(offer);
|
|
||||||
return offer;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
|
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
|
||||||
@ -266,7 +290,7 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
if (keyImagePoller != null) keyImagePoller.clearKeyImages();
|
xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -274,37 +298,131 @@ public class OfferBookService {
|
|||||||
// Private
|
// Private
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private synchronized void maybeInitializeKeyImagePoller() {
|
private void announceOfferAdded(Offer offer) {
|
||||||
if (keyImagePoller != null) return;
|
xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
|
||||||
keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
|
updateReservedFundsSpentStatus(offer);
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
// handle when key images spent
|
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
|
||||||
keyImagePoller.addListener(new XmrKeyImageListener() {
|
|
||||||
@Override
|
|
||||||
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
|
|
||||||
UserThread.execute(() -> {
|
|
||||||
for (String keyImage : spentStatuses.keySet()) {
|
|
||||||
updateAffectedOffers(keyImage);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// first poll after 20s
|
|
||||||
// TODO: remove?
|
|
||||||
new Thread(() -> {
|
|
||||||
HavenoUtils.waitFor(20000);
|
|
||||||
keyImagePoller.poll();
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getKeyImageRefreshPeriodMs() {
|
private void announceOfferRemoved(Offer offer) {
|
||||||
return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
|
updateReservedFundsSpentStatus(offer);
|
||||||
|
removeKeyImages(offer);
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
|
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasValidOffer(String offerId) {
|
||||||
|
for (Offer offer : getOffers()) {
|
||||||
|
if (offer.getId().equals(offerId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceValidOffer(Offer offer) {
|
||||||
|
synchronized (validOffers) {
|
||||||
|
removeValidOffer(offer.getId());
|
||||||
|
validOffers.add(offer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceInvalidOffer(Offer offer) {
|
||||||
|
synchronized (invalidOffers) {
|
||||||
|
removeInvalidOffer(offer.getId());
|
||||||
|
invalidOffers.add(offer);
|
||||||
|
|
||||||
|
// remove invalid offer after timeout
|
||||||
|
synchronized (invalidOfferTimers) {
|
||||||
|
Timer timer = invalidOfferTimers.get(offer.getId());
|
||||||
|
if (timer != null) timer.stop();
|
||||||
|
timer = UserThread.runAfter(() -> {
|
||||||
|
removeInvalidOffer(offer.getId());
|
||||||
|
}, INVALID_OFFERS_TIMEOUT);
|
||||||
|
invalidOfferTimers.put(offer.getId(), timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeValidOffer(String offerId) {
|
||||||
|
synchronized (validOffers) {
|
||||||
|
validOffers.removeIf(offer -> offer.getId().equals(offerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeInvalidOffer(String offerId) {
|
||||||
|
synchronized (invalidOffers) {
|
||||||
|
invalidOffers.removeIf(offer -> offer.getId().equals(offerId));
|
||||||
|
|
||||||
|
// remove timeout
|
||||||
|
synchronized (invalidOfferTimers) {
|
||||||
|
Timer timer = invalidOfferTimers.get(offerId);
|
||||||
|
if (timer != null) timer.stop();
|
||||||
|
invalidOfferTimers.remove(offerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOfferPayload(OfferPayload offerPayload) {
|
||||||
|
|
||||||
|
// validate offer is not banned
|
||||||
|
if (filterManager.isOfferIdBanned(offerPayload.getId())) {
|
||||||
|
throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate v3 node address compliance
|
||||||
|
boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName());
|
||||||
|
if (!isV3NodeAddressCompliant) {
|
||||||
|
throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate against existing offers
|
||||||
|
synchronized (validOffers) {
|
||||||
|
int numOffersWithSharedKeyImages = 0;
|
||||||
|
for (Offer offer : validOffers) {
|
||||||
|
|
||||||
|
// validate that no offer has overlapping but different key images
|
||||||
|
if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
|
||||||
|
!Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) {
|
||||||
|
throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate that no offer has same key images, payment method, and currency
|
||||||
|
if (!offer.getId().equals(offerPayload.getId()) &&
|
||||||
|
offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
|
||||||
|
offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) &&
|
||||||
|
offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) &&
|
||||||
|
offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) {
|
||||||
|
throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// count offers with same key images
|
||||||
|
if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate max offers with same key images
|
||||||
|
if (numOffersWithSharedKeyImages > Restrictions.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeKeyImages(Offer offer) {
|
||||||
|
Set<String> unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages());
|
||||||
|
synchronized (validOffers) {
|
||||||
|
for (Offer validOffer : validOffers) {
|
||||||
|
if (validOffer.getId().equals(offer.getId())) continue;
|
||||||
|
unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAffectedOffers(String keyImage) {
|
private void updateAffectedOffers(String keyImage) {
|
||||||
for (Offer offer : getOffers()) {
|
for (Offer offer : getOffers()) {
|
||||||
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
||||||
|
updateReservedFundsSpentStatus(offer);
|
||||||
synchronized (offerBookChangedListeners) {
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.forEach(listener -> {
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
listener.onRemoved(offer);
|
listener.onRemoved(offer);
|
||||||
@ -315,10 +433,9 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setReservedFundsSpent(Offer offer) {
|
private void updateReservedFundsSpentStatus(Offer offer) {
|
||||||
if (keyImagePoller == null) return;
|
|
||||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||||
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
|
if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
|
||||||
offer.setReservedFundsSpent(true);
|
offer.setReservedFundsSpent(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,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.getCurrencyCode(), 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={}, ",
|
||||||
|
@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
|
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
|
||||||
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
|
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
|
||||||
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
|
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
|
||||||
|
public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo";
|
||||||
|
|
||||||
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
|
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
|
||||||
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
|
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
|
||||||
@ -156,7 +157,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
|
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
|
||||||
private final boolean isPrivateOffer;
|
private final boolean isPrivateOffer;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String hashOfChallenge;
|
private final String challengeHash;
|
||||||
|
@Nullable
|
||||||
|
private final String extraInfo;
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -195,12 +198,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
long lowerClosePrice,
|
long lowerClosePrice,
|
||||||
long upperClosePrice,
|
long upperClosePrice,
|
||||||
boolean isPrivateOffer,
|
boolean isPrivateOffer,
|
||||||
@Nullable String hashOfChallenge,
|
@Nullable String challengeHash,
|
||||||
@Nullable Map<String, String> extraDataMap,
|
@Nullable Map<String, String> extraDataMap,
|
||||||
int protocolVersion,
|
int protocolVersion,
|
||||||
@Nullable NodeAddress arbitratorSigner,
|
@Nullable NodeAddress arbitratorSigner,
|
||||||
@Nullable byte[] arbitratorSignature,
|
@Nullable byte[] arbitratorSignature,
|
||||||
@Nullable List<String> reserveTxKeyImages) {
|
@Nullable List<String> reserveTxKeyImages,
|
||||||
|
@Nullable String extraInfo) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.ownerNodeAddress = ownerNodeAddress;
|
this.ownerNodeAddress = ownerNodeAddress;
|
||||||
@ -238,7 +242,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
this.lowerClosePrice = lowerClosePrice;
|
this.lowerClosePrice = lowerClosePrice;
|
||||||
this.upperClosePrice = upperClosePrice;
|
this.upperClosePrice = upperClosePrice;
|
||||||
this.isPrivateOffer = isPrivateOffer;
|
this.isPrivateOffer = isPrivateOffer;
|
||||||
this.hashOfChallenge = hashOfChallenge;
|
this.challengeHash = challengeHash;
|
||||||
|
this.extraInfo = extraInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getHash() {
|
public byte[] getHash() {
|
||||||
@ -284,12 +289,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
lowerClosePrice,
|
lowerClosePrice,
|
||||||
upperClosePrice,
|
upperClosePrice,
|
||||||
isPrivateOffer,
|
isPrivateOffer,
|
||||||
hashOfChallenge,
|
challengeHash,
|
||||||
extraDataMap,
|
extraDataMap,
|
||||||
protocolVersion,
|
protocolVersion,
|
||||||
arbitratorSigner,
|
arbitratorSigner,
|
||||||
null,
|
null,
|
||||||
reserveTxKeyImages
|
reserveTxKeyImages,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
return signee.getHash();
|
return signee.getHash();
|
||||||
@ -328,12 +334,21 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
|
|
||||||
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
||||||
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
|
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
|
||||||
return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted);
|
boolean isBuyerTaker = getDirection() == OfferDirection.SELL;
|
||||||
|
if (isPrivateOffer() && isBuyerTaker) {
|
||||||
|
return securityDepositUnadjusted;
|
||||||
|
} else {
|
||||||
|
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
|
||||||
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
|
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
|
||||||
return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted);
|
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuyerAsTakerWithoutDeposit() {
|
||||||
|
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -376,11 +391,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
|
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
|
||||||
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
|
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
|
||||||
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
|
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
|
||||||
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
|
Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash);
|
||||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||||
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
|
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
|
||||||
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
|
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
|
||||||
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
|
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
|
||||||
|
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
|
||||||
|
|
||||||
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
||||||
}
|
}
|
||||||
@ -392,7 +408,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||||
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
||||||
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
||||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
|
||||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||||
null : proto.getExtraDataMap();
|
null : proto.getExtraDataMap();
|
||||||
|
|
||||||
@ -428,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
proto.getLowerClosePrice(),
|
proto.getLowerClosePrice(),
|
||||||
proto.getUpperClosePrice(),
|
proto.getUpperClosePrice(),
|
||||||
proto.getIsPrivateOffer(),
|
proto.getIsPrivateOffer(),
|
||||||
hashOfChallenge,
|
ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
|
||||||
extraDataMapMap,
|
extraDataMapMap,
|
||||||
proto.getProtocolVersion(),
|
proto.getProtocolVersion(),
|
||||||
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
||||||
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
||||||
reserveTxKeyImages);
|
reserveTxKeyImages,
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -475,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
",\r\n lowerClosePrice=" + lowerClosePrice +
|
",\r\n lowerClosePrice=" + lowerClosePrice +
|
||||||
",\r\n upperClosePrice=" + upperClosePrice +
|
",\r\n upperClosePrice=" + upperClosePrice +
|
||||||
",\r\n isPrivateOffer=" + isPrivateOffer +
|
",\r\n isPrivateOffer=" + isPrivateOffer +
|
||||||
",\r\n hashOfChallenge='" + hashOfChallenge + '\'' +
|
",\r\n challengeHash='" + challengeHash +
|
||||||
",\r\n arbitratorSigner=" + arbitratorSigner +
|
",\r\n arbitratorSigner=" + arbitratorSigner +
|
||||||
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
||||||
|
",\r\n extraInfo='" + extraInfo +
|
||||||
"\r\n} ";
|
"\r\n} ";
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
|
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
|
||||||
// The json is used for the hash in the contract and change of oder would cause a different hash and
|
// The json is used for the hash in the contract and change of order would cause a different hash and
|
||||||
// therefore a failure during trade.
|
// therefore a failure during trade.
|
||||||
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
|
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
|
||||||
@Override
|
@Override
|
||||||
@ -519,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
|
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
|
||||||
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
|
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
|
||||||
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
|
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
|
||||||
|
object.add("extraInfo", context.serialize(offerPayload.getExtraInfo()));
|
||||||
|
// reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import haveno.core.monetary.Volume;
|
|||||||
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
|
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
|
||||||
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
|
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
|
||||||
import static haveno.core.offer.OfferPayload.CAPABILITIES;
|
import static haveno.core.offer.OfferPayload.CAPABILITIES;
|
||||||
|
import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO;
|
||||||
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
|
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
|
||||||
import static haveno.core.offer.OfferPayload.F2F_CITY;
|
import static haveno.core.offer.OfferPayload.F2F_CITY;
|
||||||
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
|
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
|
||||||
@ -48,6 +49,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
|
|||||||
|
|
||||||
import haveno.core.payment.AustraliaPayidAccount;
|
import haveno.core.payment.AustraliaPayidAccount;
|
||||||
import haveno.core.payment.CashAppAccount;
|
import haveno.core.payment.CashAppAccount;
|
||||||
|
import haveno.core.payment.CashAtAtmAccount;
|
||||||
import haveno.core.payment.F2FAccount;
|
import haveno.core.payment.F2FAccount;
|
||||||
import haveno.core.payment.PayByMailAccount;
|
import haveno.core.payment.PayByMailAccount;
|
||||||
import haveno.core.payment.PayPalAccount;
|
import haveno.core.payment.PayPalAccount;
|
||||||
@ -58,8 +60,8 @@ 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.getMaxBuyerSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
|
||||||
import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
|
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
|
||||||
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,9 +122,10 @@ public class OfferUtil {
|
|||||||
|
|
||||||
public long getMaxTradeLimit(PaymentAccount paymentAccount,
|
public long getMaxTradeLimit(PaymentAccount paymentAccount,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
OfferDirection direction) {
|
OfferDirection direction,
|
||||||
|
boolean buyerAsTakerWithoutDeposit) {
|
||||||
return paymentAccount != null
|
return paymentAccount != null
|
||||||
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
|
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
|
||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,6 +219,10 @@ public class OfferUtil {
|
|||||||
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
|
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paymentAccount instanceof CashAtAtmAccount) {
|
||||||
|
extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo());
|
||||||
|
}
|
||||||
|
|
||||||
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
|
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
|
||||||
|
|
||||||
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
|
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
|
||||||
@ -228,16 +235,16 @@ public class OfferUtil {
|
|||||||
return extraDataMap.isEmpty() ? null : extraDataMap;
|
return extraDataMap.isEmpty() ? null : extraDataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateOfferData(double buyerSecurityDeposit,
|
public void validateOfferData(double securityDeposit,
|
||||||
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(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
|
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
|
||||||
"securityDeposit must not exceed " +
|
"securityDeposit must not exceed " +
|
||||||
getMaxBuyerSecurityDepositAsPercent());
|
getMaxSecurityDepositAsPercent());
|
||||||
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
|
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
|
||||||
"securityDeposit must not be less than " +
|
"securityDeposit must not be less than " +
|
||||||
getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
|
getMinSecurityDepositAsPercent() + " 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()),
|
||||||
|
@ -48,6 +48,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
public final class OpenOffer implements Tradable {
|
public final class OpenOffer implements Tradable {
|
||||||
@ -96,6 +97,9 @@ public final class OpenOffer implements Tradable {
|
|||||||
@Getter
|
@Getter
|
||||||
private String reserveTxKey;
|
private String reserveTxKey;
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String challenge;
|
||||||
|
@Getter
|
||||||
private final long triggerPrice;
|
private final long triggerPrice;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ -107,6 +111,12 @@ public final class OpenOffer implements Tradable {
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
transient int numProcessingAttempts = 0;
|
transient int numProcessingAttempts = 0;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private boolean deactivatedByTrigger;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private String groupId;
|
||||||
|
|
||||||
public OpenOffer(Offer offer) {
|
public OpenOffer(Offer offer) {
|
||||||
this(offer, 0, false);
|
this(offer, 0, false);
|
||||||
@ -120,6 +130,8 @@ public final class OpenOffer implements Tradable {
|
|||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.triggerPrice = triggerPrice;
|
this.triggerPrice = triggerPrice;
|
||||||
this.reserveExactAmount = reserveExactAmount;
|
this.reserveExactAmount = reserveExactAmount;
|
||||||
|
this.challenge = offer.getChallenge();
|
||||||
|
this.groupId = UUID.randomUUID().toString();
|
||||||
state = State.PENDING;
|
state = State.PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +149,9 @@ public final class OpenOffer implements Tradable {
|
|||||||
this.reserveTxHash = openOffer.reserveTxHash;
|
this.reserveTxHash = openOffer.reserveTxHash;
|
||||||
this.reserveTxHex = openOffer.reserveTxHex;
|
this.reserveTxHex = openOffer.reserveTxHex;
|
||||||
this.reserveTxKey = openOffer.reserveTxKey;
|
this.reserveTxKey = openOffer.reserveTxKey;
|
||||||
|
this.challenge = openOffer.challenge;
|
||||||
|
this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
|
||||||
|
this.groupId = openOffer.groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -153,7 +168,10 @@ public final class OpenOffer implements Tradable {
|
|||||||
long splitOutputTxFee,
|
long splitOutputTxFee,
|
||||||
@Nullable String reserveTxHash,
|
@Nullable String reserveTxHash,
|
||||||
@Nullable String reserveTxHex,
|
@Nullable String reserveTxHex,
|
||||||
@Nullable String reserveTxKey) {
|
@Nullable String reserveTxKey,
|
||||||
|
@Nullable String challenge,
|
||||||
|
boolean deactivatedByTrigger,
|
||||||
|
@Nullable String groupId) {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.triggerPrice = triggerPrice;
|
this.triggerPrice = triggerPrice;
|
||||||
@ -164,6 +182,10 @@ public final class OpenOffer implements Tradable {
|
|||||||
this.reserveTxHash = reserveTxHash;
|
this.reserveTxHash = reserveTxHash;
|
||||||
this.reserveTxHex = reserveTxHex;
|
this.reserveTxHex = reserveTxHex;
|
||||||
this.reserveTxKey = reserveTxKey;
|
this.reserveTxKey = reserveTxKey;
|
||||||
|
this.challenge = challenge;
|
||||||
|
this.deactivatedByTrigger = deactivatedByTrigger;
|
||||||
|
if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19)
|
||||||
|
this.groupId = groupId;
|
||||||
|
|
||||||
// reset reserved state to available
|
// reset reserved state to available
|
||||||
if (this.state == State.RESERVED) setState(State.AVAILABLE);
|
if (this.state == State.RESERVED) setState(State.AVAILABLE);
|
||||||
@ -176,7 +198,8 @@ public final class OpenOffer implements Tradable {
|
|||||||
.setTriggerPrice(triggerPrice)
|
.setTriggerPrice(triggerPrice)
|
||||||
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
|
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
|
||||||
.setSplitOutputTxFee(splitOutputTxFee)
|
.setSplitOutputTxFee(splitOutputTxFee)
|
||||||
.setReserveExactAmount(reserveExactAmount);
|
.setReserveExactAmount(reserveExactAmount)
|
||||||
|
.setDeactivatedByTrigger(deactivatedByTrigger);
|
||||||
|
|
||||||
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
|
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
|
||||||
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
|
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
|
||||||
@ -184,6 +207,8 @@ public final class OpenOffer implements Tradable {
|
|||||||
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
|
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
|
||||||
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
|
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
|
||||||
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
|
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
|
||||||
|
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
|
||||||
|
Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId));
|
||||||
|
|
||||||
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
|
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
|
||||||
}
|
}
|
||||||
@ -199,7 +224,10 @@ public final class OpenOffer implements Tradable {
|
|||||||
proto.getSplitOutputTxFee(),
|
proto.getSplitOutputTxFee(),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
|
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()));
|
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
|
||||||
|
proto.getDeactivatedByTrigger(),
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
|
||||||
return openOffer;
|
return openOffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +254,14 @@ public final class OpenOffer implements Tradable {
|
|||||||
public void setState(State state) {
|
public void setState(State state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
stateProperty.set(state);
|
stateProperty.set(state);
|
||||||
|
if (state == State.AVAILABLE) {
|
||||||
|
deactivatedByTrigger = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deactivate(boolean deactivatedByTrigger) {
|
||||||
|
this.deactivatedByTrigger = deactivatedByTrigger;
|
||||||
|
setState(State.DEACTIVATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||||
@ -257,6 +293,7 @@ public final class OpenOffer implements Tradable {
|
|||||||
",\n reserveExactAmount=" + reserveExactAmount +
|
",\n reserveExactAmount=" + reserveExactAmount +
|
||||||
",\n scheduledAmount=" + scheduledAmount +
|
",\n scheduledAmount=" + scheduledAmount +
|
||||||
",\n splitOutputTxFee=" + splitOutputTxFee +
|
",\n splitOutputTxFee=" + splitOutputTxFee +
|
||||||
|
",\n groupId=" + groupId +
|
||||||
"\n}";
|
"\n}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -47,11 +47,13 @@ public final class SignedOfferList extends PersistableListAsObservable<SignedOff
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
|
synchronized (getList()) {
|
||||||
return protobuf.PersistableEnvelope.newBuilder()
|
return protobuf.PersistableEnvelope.newBuilder()
|
||||||
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
|
.setSignedOfferList(protobuf.SignedOfferList.newBuilder()
|
||||||
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
|
.addAllSignedOffer(ProtoUtil.collectionToProto(getList(), protobuf.SignedOffer.class)))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static SignedOfferList fromProto(protobuf.SignedOfferList proto) {
|
public static SignedOfferList fromProto(protobuf.SignedOfferList proto) {
|
||||||
List<SignedOffer> list = proto.getSignedOfferList().stream()
|
List<SignedOffer> list = proto.getSignedOfferList().stream()
|
||||||
|
@ -92,12 +92,11 @@ public class TriggerPriceService {
|
|||||||
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
|
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
|
||||||
.forEach(marketPrice -> {
|
.forEach(marketPrice -> {
|
||||||
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
|
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
|
||||||
.filter(openOffer -> !openOffer.isDeactivated())
|
|
||||||
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
|
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
|
public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||||
Price price = openOffer.getOffer().getPrice();
|
Price price = openOffer.getOffer().getPrice();
|
||||||
if (price == null || marketPrice == null) {
|
if (price == null || marketPrice == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -125,13 +124,12 @@ public class TriggerPriceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
|
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
|
||||||
if (wasTriggered(marketPrice, openOffer)) {
|
|
||||||
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
String currencyCode = openOffer.getOffer().getCurrencyCode();
|
||||||
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;
|
||||||
long triggerPrice = openOffer.getTriggerPrice();
|
|
||||||
|
|
||||||
|
if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
|
||||||
log.info("Market price exceeded the trigger price of the open offer.\n" +
|
log.info("Market price exceeded the trigger price of the open offer.\n" +
|
||||||
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
|
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
|
||||||
"Market price: {};\nTrigger price: {}",
|
"Market price: {};\nTrigger price: {}",
|
||||||
@ -139,14 +137,26 @@ public class TriggerPriceService {
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
openOffer.getOffer().getDirection(),
|
openOffer.getOffer().getDirection(),
|
||||||
marketPrice.getPrice(),
|
marketPrice.getPrice(),
|
||||||
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
|
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
|
||||||
);
|
);
|
||||||
|
|
||||||
openOfferManager.deactivateOpenOffer(openOffer, () -> {
|
openOfferManager.deactivateOpenOffer(openOffer, true, () -> {
|
||||||
|
}, errorMessage -> {
|
||||||
|
});
|
||||||
|
} else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) {
|
||||||
|
log.info("Market price is back within the trigger price of the open offer.\n" +
|
||||||
|
"We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
|
||||||
|
"Market price: {};\nTrigger price: {}",
|
||||||
|
openOffer.getOffer().getShortId(),
|
||||||
|
currencyCode,
|
||||||
|
openOffer.getOffer().getDirection(),
|
||||||
|
marketPrice.getPrice(),
|
||||||
|
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
|
||||||
|
);
|
||||||
|
|
||||||
|
openOfferManager.activateOpenOffer(openOffer, () -> {
|
||||||
}, errorMessage -> {
|
}, errorMessage -> {
|
||||||
});
|
});
|
||||||
} else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
|
|
||||||
// TODO: check if open offer's reserve tx is failed or double spend seen
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
|||||||
null, // reserve tx not sent from taker to maker
|
null, // reserve tx not sent from taker to maker
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
payoutAddress);
|
payoutAddress,
|
||||||
|
null); // challenge is required when offer taken
|
||||||
|
|
||||||
// save trade request to later send to arbitrator
|
// save trade request to later send to arbitrator
|
||||||
model.setTradeRequest(tradeRequest);
|
model.setTradeRequest(tradeRequest);
|
||||||
|
@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler;
|
|||||||
import haveno.common.taskrunner.TaskRunner;
|
import haveno.common.taskrunner.TaskRunner;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.offer.messages.SignOfferResponse;
|
import haveno.core.offer.messages.SignOfferResponse;
|
||||||
import haveno.core.offer.placeoffer.tasks.AddToOfferBook;
|
import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook;
|
||||||
import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse;
|
import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse;
|
||||||
import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
|
import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
|
||||||
import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest;
|
import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest;
|
||||||
@ -31,6 +31,8 @@ import haveno.core.offer.placeoffer.tasks.ValidateOffer;
|
|||||||
import haveno.core.trade.handlers.TransactionResultHandler;
|
import haveno.core.trade.handlers.TransactionResultHandler;
|
||||||
import haveno.core.trade.protocol.TradeProtocol;
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -39,8 +41,8 @@ public class PlaceOfferProtocol {
|
|||||||
|
|
||||||
private final PlaceOfferModel model;
|
private final PlaceOfferModel model;
|
||||||
private Timer timeoutTimer;
|
private Timer timeoutTimer;
|
||||||
private final TransactionResultHandler resultHandler;
|
private TransactionResultHandler resultHandler;
|
||||||
private final ErrorMessageHandler errorMessageHandler;
|
private ErrorMessageHandler errorMessageHandler;
|
||||||
private TaskRunner<PlaceOfferModel> taskRunner;
|
private TaskRunner<PlaceOfferModel> taskRunner;
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +91,6 @@ public class PlaceOfferProtocol {
|
|||||||
handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle
|
handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (woodser): switch to fluent
|
|
||||||
public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) {
|
public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) {
|
||||||
log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId());
|
log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId());
|
||||||
model.setSignOfferResponse(response);
|
model.setSignOfferResponse(response);
|
||||||
@ -119,7 +120,7 @@ public class PlaceOfferProtocol {
|
|||||||
() -> {
|
() -> {
|
||||||
log.debug("sequence at handleSignOfferResponse completed");
|
log.debug("sequence at handleSignOfferResponse completed");
|
||||||
stopTimeoutTimer();
|
stopTimeoutTimer();
|
||||||
resultHandler.handleResult(model.getTransaction()); // TODO (woodser): XMR transaction instead
|
handleResult(model.getTransaction()); // TODO: use XMR transaction instead
|
||||||
},
|
},
|
||||||
(errorMessage) -> {
|
(errorMessage) -> {
|
||||||
if (model.isOfferAddedToOfferBook()) {
|
if (model.isOfferAddedToOfferBook()) {
|
||||||
@ -135,27 +136,33 @@ public class PlaceOfferProtocol {
|
|||||||
);
|
);
|
||||||
taskRunner.addTasks(
|
taskRunner.addTasks(
|
||||||
MakerProcessSignOfferResponse.class,
|
MakerProcessSignOfferResponse.class,
|
||||||
AddToOfferBook.class
|
MaybeAddToOfferBook.class
|
||||||
);
|
);
|
||||||
|
|
||||||
taskRunner.run();
|
taskRunner.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startTimeoutTimer() {
|
public synchronized void startTimeoutTimer() {
|
||||||
|
if (resultHandler == null) return;
|
||||||
stopTimeoutTimer();
|
stopTimeoutTimer();
|
||||||
timeoutTimer = UserThread.runAfter(() -> {
|
timeoutTimer = UserThread.runAfter(() -> {
|
||||||
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
||||||
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopTimeoutTimer() {
|
private synchronized void stopTimeoutTimer() {
|
||||||
if (timeoutTimer != null) {
|
if (timeoutTimer != null) {
|
||||||
timeoutTimer.stop();
|
timeoutTimer.stop();
|
||||||
timeoutTimer = null;
|
timeoutTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleError(String errorMessage) {
|
private synchronized void handleResult(Transaction transaction) {
|
||||||
|
resultHandler.handleResult(transaction);
|
||||||
|
resetHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void handleError(String errorMessage) {
|
||||||
if (timeoutTimer != null) {
|
if (timeoutTimer != null) {
|
||||||
taskRunner.cancel();
|
taskRunner.cancel();
|
||||||
if (!model.getOpenOffer().isCanceled()) {
|
if (!model.getOpenOffer().isCanceled()) {
|
||||||
@ -164,5 +171,11 @@ public class PlaceOfferProtocol {
|
|||||||
stopTimeoutTimer();
|
stopTimeoutTimer();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
}
|
}
|
||||||
|
resetHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void resetHandlers() {
|
||||||
|
resultHandler = null;
|
||||||
|
errorMessageHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,9 @@ package haveno.core.offer.placeoffer.tasks;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import haveno.common.taskrunner.Task;
|
import haveno.common.taskrunner.Task;
|
||||||
import haveno.common.taskrunner.TaskRunner;
|
import haveno.common.taskrunner.TaskRunner;
|
||||||
@ -33,6 +35,7 @@ import haveno.core.xmr.model.XmrAddressEntry;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import monero.common.MoneroRpcConnection;
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.daemon.model.MoneroOutput;
|
import monero.daemon.model.MoneroOutput;
|
||||||
|
import monero.wallet.model.MoneroOutputWallet;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -62,7 +65,6 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
|
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
|
||||||
|
|
||||||
// create reserve tx
|
// create reserve tx
|
||||||
MoneroTxWallet reserveTx = null;
|
|
||||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
|
|
||||||
// reset protocol timeout
|
// reset protocol timeout
|
||||||
@ -78,7 +80,14 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
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();
|
||||||
|
|
||||||
|
// copy address entries to clones
|
||||||
|
for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
|
||||||
|
if (offerClone.getId().equals(offer.getId())) continue; // skip self
|
||||||
|
model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId());
|
||||||
|
}
|
||||||
|
|
||||||
// attempt creating reserve tx
|
// attempt creating reserve tx
|
||||||
|
MoneroTxWallet reserveTx = null;
|
||||||
try {
|
try {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
@ -86,6 +95,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
try {
|
try {
|
||||||
//if (true) throw new RuntimeException("Pretend error");
|
//if (true) throw new RuntimeException("Pretend error");
|
||||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
|
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage());
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} 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);
|
||||||
@ -116,11 +128,43 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
List<String> reservedKeyImages = new ArrayList<String>();
|
List<String> reservedKeyImages = new ArrayList<String>();
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||||
|
|
||||||
// update offer state
|
// update offer state including clones
|
||||||
|
if (openOffer.getGroupId() == null) {
|
||||||
openOffer.setReserveTxHash(reserveTx.getHash());
|
openOffer.setReserveTxHash(reserveTx.getHash());
|
||||||
openOffer.setReserveTxHex(reserveTx.getFullHex());
|
openOffer.setReserveTxHex(reserveTx.getFullHex());
|
||||||
openOffer.setReserveTxKey(reserveTx.getKey());
|
openOffer.setReserveTxKey(reserveTx.getKey());
|
||||||
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||||
|
} else {
|
||||||
|
for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
|
||||||
|
offerClone.setReserveTxHash(reserveTx.getHash());
|
||||||
|
offerClone.setReserveTxHex(reserveTx.getFullHex());
|
||||||
|
offerClone.setReserveTxKey(reserveTx.getKey());
|
||||||
|
offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset offer funding address entries if unused
|
||||||
|
if (fundingEntry != null) {
|
||||||
|
|
||||||
|
// get reserve tx inputs
|
||||||
|
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
|
||||||
|
|
||||||
|
// collect subaddress indices of inputs
|
||||||
|
Set<Integer> inputSubaddressIndices = new HashSet<>();
|
||||||
|
for (MoneroOutputWallet input : inputs) {
|
||||||
|
if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
// swap funding address entries to available if unused
|
||||||
|
for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
|
||||||
|
XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
|
||||||
|
if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
|
||||||
|
if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
|
||||||
|
model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
|||||||
offer.getOfferPayload().getReserveTxKeyImages(),
|
offer.getOfferPayload().getReserveTxKeyImages(),
|
||||||
returnAddress);
|
returnAddress);
|
||||||
|
|
||||||
// send request to least used arbitrators until success
|
// send request to random arbitrators until success
|
||||||
sendSignOfferRequests(request, () -> {
|
sendSignOfferRequests(request, () -> {
|
||||||
complete();
|
complete();
|
||||||
}, (errorMessage) -> {
|
}, (errorMessage) -> {
|
||||||
|
@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks;
|
|||||||
import haveno.common.taskrunner.Task;
|
import haveno.common.taskrunner.Task;
|
||||||
import haveno.common.taskrunner.TaskRunner;
|
import haveno.common.taskrunner.TaskRunner;
|
||||||
import haveno.core.offer.Offer;
|
import haveno.core.offer.Offer;
|
||||||
|
import haveno.core.offer.OpenOffer;
|
||||||
import haveno.core.offer.placeoffer.PlaceOfferModel;
|
import haveno.core.offer.placeoffer.PlaceOfferModel;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
public class AddToOfferBook extends Task<PlaceOfferModel> {
|
public class MaybeAddToOfferBook extends Task<PlaceOfferModel> {
|
||||||
|
|
||||||
public AddToOfferBook(TaskRunner<PlaceOfferModel> taskHandler, PlaceOfferModel model) {
|
public MaybeAddToOfferBook(TaskRunner<PlaceOfferModel> taskHandler, PlaceOfferModel model) {
|
||||||
super(taskHandler, model);
|
super(taskHandler, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,17 +36,32 @@ public class AddToOfferBook extends Task<PlaceOfferModel> {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId());
|
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId());
|
||||||
|
|
||||||
|
// deactivate if conflicting offer exists
|
||||||
|
if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) {
|
||||||
|
model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED);
|
||||||
|
model.setOfferAddedToOfferBook(false);
|
||||||
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to offer book and activate if pending or available
|
||||||
|
if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) {
|
||||||
model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
|
model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
|
||||||
() -> {
|
() -> {
|
||||||
|
model.getOpenOffer().setState(OpenOffer.State.AVAILABLE);
|
||||||
model.setOfferAddedToOfferBook(true);
|
model.setOfferAddedToOfferBook(true);
|
||||||
complete();
|
complete();
|
||||||
},
|
},
|
||||||
errorMessage -> {
|
errorMessage -> {
|
||||||
model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
|
model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
|
||||||
"Please check your network connection and try again.");
|
"Please check your network connection and try again.");
|
||||||
|
|
||||||
failed(errorMessage);
|
failed(errorMessage);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" +
|
model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" +
|
||||||
"Error message:\n"
|
"Error message:\n"
|
@ -21,6 +21,7 @@ import haveno.common.taskrunner.Task;
|
|||||||
import haveno.common.taskrunner.TaskRunner;
|
import haveno.common.taskrunner.TaskRunner;
|
||||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
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.placeoffer.PlaceOfferModel;
|
import haveno.core.offer.placeoffer.PlaceOfferModel;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.messages.TradeMessage;
|
import haveno.core.trade.messages.TradeMessage;
|
||||||
@ -63,8 +64,21 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
|||||||
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
|
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
|
||||||
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
|
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
|
||||||
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
|
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
|
||||||
|
offer.isPrivateOffer();
|
||||||
|
if (offer.isPrivateOffer()) {
|
||||||
|
boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY;
|
||||||
|
if (isBuyerMaker) {
|
||||||
|
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
|
||||||
|
if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct());
|
||||||
|
} else {
|
||||||
|
if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct());
|
||||||
|
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
|
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
|
||||||
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
|
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// We remove those checks to be more flexible with future changes.
|
// We remove those checks to be more flexible with future changes.
|
||||||
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
|
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
|
||||||
@ -82,9 +96,9 @@ 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());
|
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
|
||||||
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
|
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
|
||||||
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " 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");
|
||||||
|
|
||||||
checkNotNull(offer.getPrice(), "Price is null");
|
checkNotNull(offer.getPrice(), "Price is null");
|
||||||
|
@ -148,7 +148,8 @@ public class TakeOfferModel implements Model {
|
|||||||
private long getMaxTradeLimit() {
|
private long getMaxTradeLimit() {
|
||||||
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||||
offer.getCurrencyCode(),
|
offer.getCurrencyCode(),
|
||||||
offer.getMirroredDirection());
|
offer.getMirroredDirection(),
|
||||||
|
offer.hasBuyerAsTakerWithoutDeposit());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ -31,7 +31,34 @@ import java.util.List;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public final class AliPayAccount extends PaymentAccount {
|
public final class AliPayAccount extends PaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
|
||||||
|
new TraditionalCurrency("AED"),
|
||||||
|
new TraditionalCurrency("AUD"),
|
||||||
|
new TraditionalCurrency("CAD"),
|
||||||
|
new TraditionalCurrency("CHF"),
|
||||||
|
new TraditionalCurrency("CNY"),
|
||||||
|
new TraditionalCurrency("CZK"),
|
||||||
|
new TraditionalCurrency("DKK"),
|
||||||
|
new TraditionalCurrency("EUR"),
|
||||||
|
new TraditionalCurrency("GBP"),
|
||||||
|
new TraditionalCurrency("HKD"),
|
||||||
|
new TraditionalCurrency("IDR"),
|
||||||
|
new TraditionalCurrency("ILS"),
|
||||||
|
new TraditionalCurrency("JPY"),
|
||||||
|
new TraditionalCurrency("KRW"),
|
||||||
|
new TraditionalCurrency("LKR"),
|
||||||
|
new TraditionalCurrency("MUR"),
|
||||||
|
new TraditionalCurrency("MYR"),
|
||||||
|
new TraditionalCurrency("NOK"),
|
||||||
|
new TraditionalCurrency("NZD"),
|
||||||
|
new TraditionalCurrency("PHP"),
|
||||||
|
new TraditionalCurrency("RUB"),
|
||||||
|
new TraditionalCurrency("SEK"),
|
||||||
|
new TraditionalCurrency("SGD"),
|
||||||
|
new TraditionalCurrency("THB"),
|
||||||
|
new TraditionalCurrency("USD"),
|
||||||
|
new TraditionalCurrency("ZAR")
|
||||||
|
);
|
||||||
|
|
||||||
public AliPayAccount() {
|
public AliPayAccount() {
|
||||||
super(PaymentMethod.ALI_PAY);
|
super(PaymentMethod.ALI_PAY);
|
||||||
|
@ -93,7 +93,7 @@ public final class F2FAccount extends CountryBasedPaymentAccount {
|
|||||||
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
|
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
|
||||||
if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city"));
|
if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city"));
|
||||||
if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact"));
|
if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact"));
|
||||||
if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt"));
|
if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount"));
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ package haveno.core.payment;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
import haveno.common.proto.ProtoUtil;
|
import haveno.common.proto.ProtoUtil;
|
||||||
import haveno.common.proto.persistable.PersistablePayload;
|
import haveno.common.proto.persistable.PersistablePayload;
|
||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
@ -341,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload {
|
|||||||
// ---------------------------- SERIALIZATION -----------------------------
|
// ---------------------------- SERIALIZATION -----------------------------
|
||||||
|
|
||||||
public String toJson() {
|
public String toJson() {
|
||||||
Map<String, Object> jsonMap = new HashMap<String, Object>();
|
Gson gson = gsonBuilder.create();
|
||||||
if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class));
|
Map<String, Object> jsonMap = new HashMap<>();
|
||||||
|
|
||||||
|
if (paymentAccountPayload != null) {
|
||||||
|
String payloadJson = paymentAccountPayload.toJson();
|
||||||
|
Map<String, Object> payloadMap = gson.fromJson(payloadJson, new TypeToken<Map<String, Object>>() {}.getType());
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof List) {
|
||||||
|
List<?> list = (List<?>) value;
|
||||||
|
String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(","));
|
||||||
|
entry.setValue(joinedString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonMap.putAll(payloadMap);
|
||||||
|
}
|
||||||
|
|
||||||
jsonMap.put("accountName", getAccountName());
|
jsonMap.put("accountName", getAccountName());
|
||||||
jsonMap.put("accountId", getId());
|
jsonMap.put("accountId", getId());
|
||||||
if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex());
|
if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex());
|
||||||
return gsonBuilder.create().toJson(jsonMap);
|
return gson.toJson(jsonMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,6 +396,7 @@ public abstract class PaymentAccount implements PersistablePayload {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds();
|
public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public PaymentAccountForm toForm() {
|
public PaymentAccountForm toForm() {
|
||||||
|
|
||||||
// convert to json map
|
// convert to json map
|
||||||
|
@ -136,6 +136,8 @@ public class PaymentAccountFactory {
|
|||||||
return new CashAppAccount();
|
return new CashAppAccount();
|
||||||
case PaymentMethod.VENMO_ID:
|
case PaymentMethod.VENMO_ID:
|
||||||
return new VenmoAccount();
|
return new VenmoAccount();
|
||||||
|
case PaymentMethod.PAYSAFE_ID:
|
||||||
|
return new PaysafeAccount();
|
||||||
|
|
||||||
// Cannot be deleted as it would break old trade history entries
|
// Cannot be deleted as it would break old trade history entries
|
||||||
case PaymentMethod.OK_PAY_ID:
|
case PaymentMethod.OK_PAY_ID:
|
||||||
|
@ -36,11 +36,13 @@ public class PaymentAccountList extends PersistableList<PaymentAccount> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
|
synchronized (getList()) {
|
||||||
return protobuf.PersistableEnvelope.newBuilder()
|
return protobuf.PersistableEnvelope.newBuilder()
|
||||||
.setPaymentAccountList(protobuf.PaymentAccountList.newBuilder()
|
.setPaymentAccountList(protobuf.PaymentAccountList.newBuilder()
|
||||||
.addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList())))
|
.addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList())))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) {
|
public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) {
|
||||||
return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream()
|
return new PaymentAccountList(new ArrayList<>(proto.getPaymentAccountList().stream()
|
||||||
|
@ -24,7 +24,6 @@ import com.google.gson.stream.JsonToken;
|
|||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
import haveno.core.locale.Country;
|
import haveno.core.locale.Country;
|
||||||
import haveno.core.locale.CountryUtil;
|
import haveno.core.locale.CountryUtil;
|
||||||
import haveno.core.locale.TraditionalCurrency;
|
|
||||||
import haveno.core.locale.Res;
|
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;
|
||||||
@ -42,7 +41,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy;
|
import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy;
|
||||||
import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString;
|
import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString;
|
||||||
import static haveno.common.util.ReflectionUtils.handleSetFieldValueError;
|
import static haveno.common.util.ReflectionUtils.handleSetFieldValueError;
|
||||||
@ -50,7 +48,6 @@ import static haveno.common.util.ReflectionUtils.isSetterOnClass;
|
|||||||
import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy;
|
import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy;
|
||||||
import static haveno.common.util.Utilities.decodeFromHex;
|
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.getCurrencyByCountryCode;
|
|
||||||
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.MONEY_GRAM_ID;
|
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
|
||||||
@ -435,8 +432,10 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
|
|||||||
|
|
||||||
if (account.isCountryBasedPaymentAccount()) {
|
if (account.isCountryBasedPaymentAccount()) {
|
||||||
((CountryBasedPaymentAccount) account).setCountry(country.get());
|
((CountryBasedPaymentAccount) account).setCountry(country.get());
|
||||||
TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
|
|
||||||
account.setSingleTradeCurrency(fiatCurrency);
|
// TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere?
|
||||||
|
// TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
|
||||||
|
// 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 {
|
} else {
|
||||||
|
@ -124,7 +124,7 @@ public class PaymentAccountUtil {
|
|||||||
AccountAgeWitnessService accountAgeWitnessService) {
|
AccountAgeWitnessService accountAgeWitnessService) {
|
||||||
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
|
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
|
||||||
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||||
offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact();
|
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
|
||||||
return !hasChargebackRisk || hasValidAccountAgeWitness;
|
return !hasChargebackRisk || hasValidAccountAgeWitness;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
112
core/src/main/java/haveno/core/payment/PaysafeAccount.java
Normal file
112
core/src/main/java/haveno/core/payment/PaysafeAccount.java
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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.core.payment;
|
||||||
|
|
||||||
|
import haveno.core.api.model.PaymentAccountFormField;
|
||||||
|
import haveno.core.locale.TraditionalCurrency;
|
||||||
|
import haveno.core.locale.TradeCurrency;
|
||||||
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
|
import haveno.core.payment.payload.PaysafeAccountPayload;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public final class PaysafeAccount extends PaymentAccount {
|
||||||
|
|
||||||
|
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||||
|
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||||
|
PaymentAccountFormField.FieldId.EMAIL,
|
||||||
|
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||||
|
PaymentAccountFormField.FieldId.SALT
|
||||||
|
);
|
||||||
|
|
||||||
|
// https://developer.paysafe.com/en/support/reference-information/codes/
|
||||||
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
|
||||||
|
new TraditionalCurrency("AED"),
|
||||||
|
new TraditionalCurrency("ARS"),
|
||||||
|
new TraditionalCurrency("AUD"),
|
||||||
|
new TraditionalCurrency("BGN"),
|
||||||
|
new TraditionalCurrency("BRL"),
|
||||||
|
new TraditionalCurrency("CAD"),
|
||||||
|
new TraditionalCurrency("CHF"),
|
||||||
|
new TraditionalCurrency("CZK"),
|
||||||
|
new TraditionalCurrency("DKK"),
|
||||||
|
new TraditionalCurrency("EGP"),
|
||||||
|
new TraditionalCurrency("EUR"),
|
||||||
|
new TraditionalCurrency("GBP"),
|
||||||
|
new TraditionalCurrency("GEL"),
|
||||||
|
new TraditionalCurrency("HUF"),
|
||||||
|
new TraditionalCurrency("ILS"),
|
||||||
|
new TraditionalCurrency("INR"),
|
||||||
|
new TraditionalCurrency("JPY"),
|
||||||
|
new TraditionalCurrency("ISK"),
|
||||||
|
new TraditionalCurrency("KWD"),
|
||||||
|
new TraditionalCurrency("KRW"),
|
||||||
|
new TraditionalCurrency("MXN"),
|
||||||
|
new TraditionalCurrency("NOK"),
|
||||||
|
new TraditionalCurrency("NZD"),
|
||||||
|
new TraditionalCurrency("PEN"),
|
||||||
|
new TraditionalCurrency("PHP"),
|
||||||
|
new TraditionalCurrency("PLN"),
|
||||||
|
new TraditionalCurrency("RON"),
|
||||||
|
new TraditionalCurrency("RSD"),
|
||||||
|
new TraditionalCurrency("RUB"),
|
||||||
|
new TraditionalCurrency("SAR"),
|
||||||
|
new TraditionalCurrency("SEK"),
|
||||||
|
new TraditionalCurrency("TRY"),
|
||||||
|
new TraditionalCurrency("USD"),
|
||||||
|
new TraditionalCurrency("UYU")
|
||||||
|
);
|
||||||
|
|
||||||
|
public PaysafeAccount() {
|
||||||
|
super(PaymentMethod.PAYSAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PaymentAccountPayload createPayload() {
|
||||||
|
return new PaysafeAccountPayload(paymentMethod.getId(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<TradeCurrency> getSupportedCurrencies() {
|
||||||
|
return SUPPORTED_CURRENCIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||||
|
return INPUT_FIELD_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String accountId) {
|
||||||
|
((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return ((PaysafeAccountPayload) paymentAccountPayload).getEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
|
||||||
|
var field = super.getEmptyFormField(fieldId);
|
||||||
|
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue("");
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class TradeLimits {
|
public class TradeLimits {
|
||||||
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that.
|
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that.
|
||||||
|
private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Getter
|
@Getter
|
||||||
private static TradeLimits INSTANCE;
|
private static TradeLimits INSTANCE;
|
||||||
@ -57,6 +59,15 @@ public class TradeLimits {
|
|||||||
return MAX_TRADE_LIMIT;
|
return MAX_TRADE_LIMIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum trade limit without a buyer deposit.
|
||||||
|
*
|
||||||
|
* @return the maximum trade limit for a buyer without a deposit
|
||||||
|
*/
|
||||||
|
public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() {
|
||||||
|
return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT;
|
||||||
|
}
|
||||||
|
|
||||||
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
|
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
|
||||||
// age witness is not considered anymore (> 2 months).
|
// age witness is not considered anymore (> 2 months).
|
||||||
|
|
||||||
|
@ -31,11 +31,15 @@ import java.util.List;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public final class WeChatPayAccount extends PaymentAccount {
|
public final class WeChatPayAccount extends PaymentAccount {
|
||||||
|
|
||||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
|
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
|
||||||
|
new TraditionalCurrency("CNY"),
|
||||||
|
new TraditionalCurrency("USD"),
|
||||||
|
new TraditionalCurrency("EUR"),
|
||||||
|
new TraditionalCurrency("GBP")
|
||||||
|
);
|
||||||
|
|
||||||
public WeChatPayAccount() {
|
public WeChatPayAccount() {
|
||||||
super(PaymentMethod.WECHAT_PAY);
|
super(PaymentMethod.WECHAT_PAY);
|
||||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -51,6 +51,7 @@ import haveno.core.payment.CashAppAccount;
|
|||||||
import haveno.core.payment.CashAtAtmAccount;
|
import haveno.core.payment.CashAtAtmAccount;
|
||||||
import haveno.core.payment.PayByMailAccount;
|
import haveno.core.payment.PayByMailAccount;
|
||||||
import haveno.core.payment.PayPalAccount;
|
import haveno.core.payment.PayPalAccount;
|
||||||
|
import haveno.core.payment.PaysafeAccount;
|
||||||
import haveno.core.payment.CashDepositAccount;
|
import haveno.core.payment.CashDepositAccount;
|
||||||
import haveno.core.payment.CelPayAccount;
|
import haveno.core.payment.CelPayAccount;
|
||||||
import haveno.core.payment.ZelleAccount;
|
import haveno.core.payment.ZelleAccount;
|
||||||
@ -124,13 +125,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
|
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
|
||||||
TimeUnit.DAYS.toMillis(1);
|
TimeUnit.DAYS.toMillis(1);
|
||||||
|
|
||||||
// Default trade limits.
|
// These values are not used except to derive the associated risk factor.
|
||||||
// We initialize very early before reading persisted data. We will apply later the limit from
|
private static final BigInteger DEFAULT_TRADE_LIMIT_CRYPTO = HavenoUtils.xmrToAtomicUnits(200);
|
||||||
// the DAO param (Param.MAX_TRADE_LIMIT) but that can be only done after the dao is initialized.
|
|
||||||
// The default values will be used for deriving the
|
|
||||||
// risk factor so the relation between the risk categories stays the same as with the default values.
|
|
||||||
// We must not change those values as it could lead to invalid offers if amount becomes lower then new trade limit.
|
|
||||||
// Increasing might be ok, but needs more thought as well...
|
|
||||||
private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100);
|
private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100);
|
||||||
private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50);
|
private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50);
|
||||||
private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25);
|
private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25);
|
||||||
@ -198,6 +194,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
public static final String CASH_APP_ID = "CASH_APP";
|
public static final String CASH_APP_ID = "CASH_APP";
|
||||||
public static final String VENMO_ID = "VENMO";
|
public static final String VENMO_ID = "VENMO";
|
||||||
public static final String PAYPAL_ID = "PAYPAL";
|
public static final String PAYPAL_ID = "PAYPAL";
|
||||||
|
public static final String PAYSAFE_ID = "PAYSAFE";
|
||||||
|
|
||||||
public static PaymentMethod UPHOLD;
|
public static PaymentMethod UPHOLD;
|
||||||
public static PaymentMethod MONEY_BEAM;
|
public static PaymentMethod MONEY_BEAM;
|
||||||
@ -257,6 +254,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
public static PaymentMethod PAYPAL;
|
public static PaymentMethod PAYPAL;
|
||||||
public static PaymentMethod CASH_APP;
|
public static PaymentMethod CASH_APP;
|
||||||
public static PaymentMethod VENMO;
|
public static PaymentMethod VENMO;
|
||||||
|
public static PaymentMethod PAYSAFE;
|
||||||
|
|
||||||
// Cannot be deleted as it would break old trade history entries
|
// Cannot be deleted as it would break old trade history entries
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@ -288,7 +286,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
|
|
||||||
// Global
|
// Global
|
||||||
CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)),
|
CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)),
|
||||||
PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)),
|
PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)),
|
||||||
CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)),
|
CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)),
|
||||||
MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)),
|
MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)),
|
||||||
WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)),
|
WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)),
|
||||||
@ -327,6 +325,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)),
|
DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)),
|
||||||
PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)),
|
PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)),
|
||||||
CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)),
|
CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)),
|
||||||
|
PAYSAFE = new PaymentMethod(PaymentMethod.PAYSAFE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaysafeAccount.SUPPORTED_CURRENCIES)),
|
||||||
|
|
||||||
// Japan
|
// Japan
|
||||||
JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)),
|
JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)),
|
||||||
@ -342,10 +341,10 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)),
|
PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)),
|
||||||
|
|
||||||
// Cryptos
|
// Cryptos
|
||||||
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()),
|
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList()),
|
||||||
|
|
||||||
// Cryptos with 1 hour trade period
|
// Cryptos with 1 hour trade period
|
||||||
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList())
|
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList())
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private
|
// TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private
|
||||||
@ -369,7 +368,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
AUSTRALIA_PAYID_ID,
|
AUSTRALIA_PAYID_ID,
|
||||||
CASH_APP_ID,
|
CASH_APP_ID,
|
||||||
PAYPAL_ID,
|
PAYPAL_ID,
|
||||||
VENMO_ID);
|
VENMO_ID,
|
||||||
|
PAYSAFE_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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,17 +497,21 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We use the class field maxTradeLimit only for mapping the risk factor.
|
// We use the class field maxTradeLimit only for mapping the risk factor.
|
||||||
|
// The actual trade limit is calculated by dividing TradeLimits.MAX_TRADE_LIMIT by the
|
||||||
|
// risk factor, and then further decreasing by chargeback risk, account signing, and age.
|
||||||
long riskFactor;
|
long riskFactor;
|
||||||
if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact())
|
if (maxTradeLimit == DEFAULT_TRADE_LIMIT_CRYPTO.longValueExact())
|
||||||
riskFactor = 1;
|
riskFactor = 1;
|
||||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact())
|
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact())
|
||||||
riskFactor = 2;
|
|
||||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
|
|
||||||
riskFactor = 4;
|
riskFactor = 4;
|
||||||
|
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact())
|
||||||
|
riskFactor = 11;
|
||||||
|
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
|
||||||
|
riskFactor = 22;
|
||||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact())
|
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact())
|
||||||
riskFactor = 8;
|
riskFactor = 44;
|
||||||
else {
|
else {
|
||||||
riskFactor = 8;
|
riskFactor = 44;
|
||||||
log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " +
|
log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " +
|
||||||
"maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this);
|
"maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this);
|
||||||
}
|
}
|
||||||
@ -589,7 +593,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
id.equals(PaymentMethod.UPHOLD_ID) ||
|
id.equals(PaymentMethod.UPHOLD_ID) ||
|
||||||
id.equals(PaymentMethod.CASH_APP_ID) ||
|
id.equals(PaymentMethod.CASH_APP_ID) ||
|
||||||
id.equals(PaymentMethod.PAYPAL_ID) ||
|
id.equals(PaymentMethod.PAYPAL_ID) ||
|
||||||
id.equals(PaymentMethod.VENMO_ID);
|
id.equals(PaymentMethod.VENMO_ID) ||
|
||||||
|
id.equals(PaymentMethod.PAYSAFE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isRoundedForAtmCash(String id) {
|
public static boolean isRoundedForAtmCash(String id) {
|
||||||
@ -598,7 +603,6 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isFixedPriceOnly(String id) {
|
public static boolean isFixedPriceOnly(String id) {
|
||||||
return id.equals(PaymentMethod.CASH_AT_ATM_ID) ||
|
return id.equals(PaymentMethod.HAL_CASH_ID);
|
||||||
id.equals(PaymentMethod.HAL_CASH_ID);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* 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.core.payment.payload;
|
||||||
|
|
||||||
|
import com.google.protobuf.Message;
|
||||||
|
import haveno.core.locale.Res;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
|
public final class PaysafeAccountPayload extends PaymentAccountPayload {
|
||||||
|
private String email = "";
|
||||||
|
|
||||||
|
public PaysafeAccountPayload(String paymentMethod, String id) {
|
||||||
|
super(paymentMethod, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private PaysafeAccountPayload(String paymentMethod,
|
||||||
|
String id,
|
||||||
|
String email,
|
||||||
|
long maxTradePeriod,
|
||||||
|
Map<String, String> excludeFromJsonDataMap) {
|
||||||
|
super(paymentMethod,
|
||||||
|
id,
|
||||||
|
maxTradePeriod,
|
||||||
|
excludeFromJsonDataMap);
|
||||||
|
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Message toProtoMessage() {
|
||||||
|
return getPaymentAccountPayloadBuilder()
|
||||||
|
.setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
|
||||||
|
return new PaysafeAccountPayload(proto.getPaymentMethodId(),
|
||||||
|
proto.getId(),
|
||||||
|
proto.getPaysafeAccountPayload().getEmail(),
|
||||||
|
proto.getMaxTradePeriod(),
|
||||||
|
new HashMap<>(proto.getExcludeFromJsonDataMap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPaymentDetails() {
|
||||||
|
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPaymentDetailsForTradePopup() {
|
||||||
|
return getPaymentDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getAgeWitnessInputData() {
|
||||||
|
return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
@ -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.getMinBuyerSecurityDepositAsPercent();
|
double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
|
||||||
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.getMaxBuyerSecurityDepositAsPercent();
|
double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
|
||||||
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)));
|
||||||
|
@ -54,6 +54,7 @@ import haveno.core.payment.payload.NequiAccountPayload;
|
|||||||
import haveno.core.payment.payload.OKPayAccountPayload;
|
import haveno.core.payment.payload.OKPayAccountPayload;
|
||||||
import haveno.core.payment.payload.PaxumAccountPayload;
|
import haveno.core.payment.payload.PaxumAccountPayload;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
|
import haveno.core.payment.payload.PaysafeAccountPayload;
|
||||||
import haveno.core.payment.payload.PayPalAccountPayload;
|
import haveno.core.payment.payload.PayPalAccountPayload;
|
||||||
import haveno.core.payment.payload.PayseraAccountPayload;
|
import haveno.core.payment.payload.PayseraAccountPayload;
|
||||||
import haveno.core.payment.payload.PaytmAccountPayload;
|
import haveno.core.payment.payload.PaytmAccountPayload;
|
||||||
@ -239,6 +240,8 @@ public class CoreProtoResolver implements ProtoResolver {
|
|||||||
return VenmoAccountPayload.fromProto(proto);
|
return VenmoAccountPayload.fromProto(proto);
|
||||||
case PAYPAL_ACCOUNT_PAYLOAD:
|
case PAYPAL_ACCOUNT_PAYLOAD:
|
||||||
return PayPalAccountPayload.fromProto(proto);
|
return PayPalAccountPayload.fromProto(proto);
|
||||||
|
case PAYSAFE_ACCOUNT_PAYLOAD:
|
||||||
|
return PaysafeAccountPayload.fromProto(proto);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase);
|
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase);
|
||||||
|
@ -52,7 +52,8 @@ public class ProvidersRepository {
|
|||||||
private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/";
|
private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/";
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
||||||
private final Config config;
|
private final Config config;
|
||||||
|
@ -232,11 +232,13 @@ public abstract class SupportManager {
|
|||||||
getAllChatMessages(ackMessage.getSourceId()).stream()
|
getAllChatMessages(ackMessage.getSourceId()).stream()
|
||||||
.filter(msg -> msg.getUid().equals(ackMessage.getSourceUid()))
|
.filter(msg -> msg.getUid().equals(ackMessage.getSourceUid()))
|
||||||
.forEach(msg -> {
|
.forEach(msg -> {
|
||||||
|
UserThread.execute(() -> {
|
||||||
if (ackMessage.isSuccess())
|
if (ackMessage.isSuccess())
|
||||||
msg.setAcknowledged(true);
|
msg.setAcknowledged(true);
|
||||||
else
|
else
|
||||||
msg.setAckError(ackMessage.getErrorMessage());
|
msg.setAckError(ackMessage.getErrorMessage());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -467,7 +467,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return this.disputeState == State.OPEN || this.disputeState == State.REOPENED;
|
return isNew() || this.disputeState == State.OPEN || this.disputeState == State.REOPENED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
|
@ -74,7 +74,9 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
@Override
|
@Override
|
||||||
public void readPersisted(Runnable completeHandler) {
|
public void readPersisted(Runnable completeHandler) {
|
||||||
persistenceManager.readPersisted(getFileName(), persisted -> {
|
persistenceManager.readPersisted(getFileName(), persisted -> {
|
||||||
|
synchronized (persisted.getList()) {
|
||||||
disputeList.setAll(persisted.getList());
|
disputeList.setAll(persisted.getList());
|
||||||
|
}
|
||||||
completeHandler.run();
|
completeHandler.run();
|
||||||
},
|
},
|
||||||
completeHandler);
|
completeHandler);
|
||||||
@ -145,10 +147,13 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
private void onDisputesChangeListener(List<? extends Dispute> addedList,
|
private void onDisputesChangeListener(List<? extends Dispute> addedList,
|
||||||
@Nullable List<? extends Dispute> removedList) {
|
@Nullable List<? extends Dispute> removedList) {
|
||||||
if (removedList != null) {
|
if (removedList != null) {
|
||||||
|
synchronized (removedList) {
|
||||||
removedList.forEach(dispute -> {
|
removedList.forEach(dispute -> {
|
||||||
disputedTradeIds.remove(dispute.getTradeId());
|
disputedTradeIds.remove(dispute.getTradeId());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
synchronized (addedList) {
|
||||||
addedList.forEach(dispute -> {
|
addedList.forEach(dispute -> {
|
||||||
// for each dispute added, keep track of its "BadgeCountProperty"
|
// for each dispute added, keep track of its "BadgeCountProperty"
|
||||||
EasyBind.subscribe(dispute.getBadgeCountProperty(),
|
EasyBind.subscribe(dispute.getBadgeCountProperty(),
|
||||||
@ -166,6 +171,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
disputedTradeIds.add(dispute.getTradeId());
|
disputedTradeIds.add(dispute.getTradeId());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void requestPersistence() {
|
public void requestPersistence() {
|
||||||
persistenceManager.requestPersistence();
|
persistenceManager.requestPersistence();
|
||||||
|
@ -288,6 +288,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
cleanupDisputes();
|
cleanupDisputes();
|
||||||
|
|
||||||
List<Dispute> disputes = getDisputeList().getList();
|
List<Dispute> disputes = getDisputeList().getList();
|
||||||
|
synchronized (disputes) {
|
||||||
disputes.forEach(dispute -> {
|
disputes.forEach(dispute -> {
|
||||||
try {
|
try {
|
||||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||||
@ -296,6 +297,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
maybeClearSensitiveData();
|
maybeClearSensitiveData();
|
||||||
}
|
}
|
||||||
@ -318,12 +320,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
public void maybeClearSensitiveData() {
|
public void maybeClearSensitiveData() {
|
||||||
log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName());
|
log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName());
|
||||||
Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing();
|
Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing();
|
||||||
|
synchronized (getDisputeList().getList()) {
|
||||||
getDisputeList().getList().stream()
|
getDisputeList().getList().stream()
|
||||||
.filter(e -> e.isClosed())
|
.filter(e -> e.isClosed())
|
||||||
.filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate))
|
.filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate))
|
||||||
.forEach(Dispute::maybeClearSensitiveData);
|
.forEach(Dispute::maybeClearSensitiveData);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Dispute handling
|
// Dispute handling
|
||||||
@ -359,6 +363,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip if payout is confirmed
|
||||||
|
if (trade.isPayoutConfirmed()) {
|
||||||
|
String errorMsg = "Cannot open dispute because payout is already confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
||||||
|
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (disputeList.getObservableList()) {
|
synchronized (disputeList.getObservableList()) {
|
||||||
if (disputeList.contains(dispute)) {
|
if (disputeList.contains(dispute)) {
|
||||||
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
||||||
@ -368,8 +379,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
boolean reOpen = storedDisputeOptional.isPresent();
|
||||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
|
||||||
|
|
||||||
// add or re-open dispute
|
// add or re-open dispute
|
||||||
if (reOpen) {
|
if (reOpen) {
|
||||||
@ -393,13 +403,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
chatMessage.setSystemMessage(true);
|
chatMessage.setSystemMessage(true);
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
|
|
||||||
// export latest multisig hex
|
|
||||||
try {
|
|
||||||
trade.exportMultisigHex();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to export multisig hex", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create dispute opened message
|
// create dispute opened message
|
||||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||||
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||||
@ -472,12 +475,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
|
||||||
"TradeId = " + dispute.getTradeId();
|
|
||||||
log.warn(msg);
|
|
||||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
@ -537,15 +534,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to validate payment account
|
// try to validate payment accounts
|
||||||
try {
|
try {
|
||||||
DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing
|
DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(ExceptionUtils.getStackTrace(e));
|
log.error(ExceptionUtils.getStackTrace(e));
|
||||||
trade.prependErrorMessage(e.getMessage());
|
trade.prependErrorMessage(e.getMessage());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set arbitrator's payment account payloads
|
||||||
|
if (trade.isArbitrator()) {
|
||||||
|
if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload());
|
||||||
|
if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload());
|
||||||
|
}
|
||||||
|
|
||||||
// get sender
|
// get sender
|
||||||
TradePeer sender;
|
TradePeer sender;
|
||||||
if (reOpen) { // re-open can come from either peer
|
if (reOpen) { // re-open can come from either peer
|
||||||
@ -572,8 +575,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update multisig hex
|
// update opener's multisig hex
|
||||||
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender;
|
||||||
|
if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex());
|
||||||
|
|
||||||
// add chat message with price info
|
// add chat message with price info
|
||||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||||
@ -599,7 +603,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
if (trade.isArbitrator()) {
|
if (trade.isArbitrator()) {
|
||||||
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
|
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
|
||||||
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
|
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
|
||||||
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex());
|
||||||
}
|
}
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
|
@ -41,9 +41,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class DisputeValidation {
|
public class DisputeValidation {
|
||||||
|
|
||||||
public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException {
|
public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException {
|
||||||
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
|
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
|
||||||
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract");
|
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract");
|
||||||
|
if (dispute.getBuyerPaymentAccountPayload() != null) {
|
||||||
|
if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void validateDisputeData(Dispute dispute) throws ValidationException {
|
public static void validateDisputeData(Dispute dispute) throws ValidationException {
|
||||||
|
@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage;
|
|||||||
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
|
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
|
||||||
import haveno.core.support.messages.ChatMessage;
|
import haveno.core.support.messages.ChatMessage;
|
||||||
import haveno.core.support.messages.SupportMessage;
|
import haveno.core.support.messages.SupportMessage;
|
||||||
import haveno.core.trade.BuyerTrade;
|
|
||||||
import haveno.core.trade.ClosedTradableManager;
|
import haveno.core.trade.ClosedTradableManager;
|
||||||
import haveno.core.trade.Contract;
|
import haveno.core.trade.Contract;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
@ -177,6 +176,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
// remove disputes opened by arbitrator, which is not allowed
|
// remove disputes opened by arbitrator, which is not allowed
|
||||||
Set<Dispute> toRemoves = new HashSet<>();
|
Set<Dispute> toRemoves = new HashSet<>();
|
||||||
List<Dispute> disputes = getDisputeList().getList();
|
List<Dispute> disputes = getDisputeList().getList();
|
||||||
|
synchronized (disputes) {
|
||||||
for (Dispute dispute : disputes) {
|
for (Dispute dispute : disputes) {
|
||||||
|
|
||||||
// get dispute's trade
|
// get dispute's trade
|
||||||
@ -191,6 +191,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
toRemoves.add(dispute);
|
toRemoves.add(dispute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (Dispute toRemove : toRemoves) {
|
for (Dispute toRemove : toRemoves) {
|
||||||
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
|
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
|
||||||
getDisputeList().remove(toRemove);
|
getDisputeList().remove(toRemove);
|
||||||
@ -464,14 +465,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
// check daemon connection
|
// check daemon connection
|
||||||
trade.verifyDaemonConnection();
|
trade.verifyDaemonConnection();
|
||||||
|
|
||||||
// adapt from 1.0.6 to 1.0.7 which changes field usage
|
|
||||||
// TODO: remove after future updates to allow old trades to clear
|
|
||||||
if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
|
|
||||||
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
|
||||||
if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
|
|
||||||
trade.setPayoutTxHex(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
if (trade.getPayoutTxHex() == null) {
|
if (trade.getPayoutTxHex() == null) {
|
||||||
try {
|
try {
|
||||||
|
@ -55,9 +55,11 @@ public final class MediationDisputeList extends DisputeList<Dispute> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
|
synchronized (getList()) {
|
||||||
return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder()
|
return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder()
|
||||||
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
|
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto,
|
public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto,
|
||||||
CoreProtoResolver coreProtoResolver) {
|
CoreProtoResolver coreProtoResolver) {
|
||||||
|
@ -196,8 +196,8 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
|
||||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
|
||||||
}
|
}
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ import java.util.Optional;
|
|||||||
public final class DisputeOpenedMessage extends DisputeMessage {
|
public final class DisputeOpenedMessage extends DisputeMessage {
|
||||||
private final Dispute dispute;
|
private final Dispute dispute;
|
||||||
private final NodeAddress senderNodeAddress;
|
private final NodeAddress senderNodeAddress;
|
||||||
private final String updatedMultisigHex;
|
private final String openerUpdatedMultisigHex;
|
||||||
private final PaymentSentMessage paymentSentMessage;
|
private final PaymentSentMessage paymentSentMessage;
|
||||||
|
|
||||||
public DisputeOpenedMessage(Dispute dispute,
|
public DisputeOpenedMessage(Dispute dispute,
|
||||||
@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
|
|||||||
super(messageVersion, uid, supportType);
|
super(messageVersion, uid, supportType);
|
||||||
this.dispute = dispute;
|
this.dispute = dispute;
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
this.updatedMultisigHex = updatedMultisigHex;
|
this.openerUpdatedMultisigHex = updatedMultisigHex;
|
||||||
this.paymentSentMessage = paymentSentMessage;
|
this.paymentSentMessage = paymentSentMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
|
|||||||
.setDispute(dispute.toProtoMessage())
|
.setDispute(dispute.toProtoMessage())
|
||||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||||
.setType(SupportType.toProtoMessage(supportType))
|
.setType(SupportType.toProtoMessage(supportType))
|
||||||
.setUpdatedMultisigHex(updatedMultisigHex);
|
.setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex);
|
||||||
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||||
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
|
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
|
|||||||
proto.getUid(),
|
proto.getUid(),
|
||||||
messageVersion,
|
messageVersion,
|
||||||
SupportType.fromProto(proto.getType()),
|
SupportType.fromProto(proto.getType()),
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()),
|
||||||
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
|
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
|
|||||||
",\n DisputeOpenedMessage.uid='" + uid + '\'' +
|
",\n DisputeOpenedMessage.uid='" + uid + '\'' +
|
||||||
",\n messageVersion=" + messageVersion +
|
",\n messageVersion=" + messageVersion +
|
||||||
",\n supportType=" + supportType +
|
",\n supportType=" + supportType +
|
||||||
",\n updatedMultisigHex=" + updatedMultisigHex +
|
",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex +
|
||||||
",\n paymentSentMessage=" + paymentSentMessage +
|
",\n paymentSentMessage=" + paymentSentMessage +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
|
@ -58,10 +58,11 @@ public final class RefundDisputeList extends DisputeList<Dispute> {
|
|||||||
@Override
|
@Override
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND"));
|
forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND"));
|
||||||
|
synchronized (getList()) {
|
||||||
return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder()
|
return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder()
|
||||||
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
|
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto,
|
public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto,
|
||||||
CoreProtoResolver coreProtoResolver) {
|
CoreProtoResolver coreProtoResolver) {
|
||||||
|
@ -196,8 +196,8 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
|
||||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
|
||||||
}
|
}
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
|
|
||||||
@ -205,8 +205,8 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||||||
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
||||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
|
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
|
||||||
} else {
|
} else {
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
|
||||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trade in the context of an arbitrator.
|
* Trade in the context of an arbitrator.
|
||||||
*/
|
*/
|
||||||
@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade {
|
|||||||
String uid,
|
String uid,
|
||||||
NodeAddress makerNodeAddress,
|
NodeAddress makerNodeAddress,
|
||||||
NodeAddress takerNodeAddress,
|
NodeAddress takerNodeAddress,
|
||||||
NodeAddress arbitratorNodeAddress) {
|
NodeAddress arbitratorNodeAddress,
|
||||||
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress);
|
@Nullable String challenge) {
|
||||||
|
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade {
|
|||||||
uid,
|
uid,
|
||||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
|
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
|
||||||
proto,
|
proto,
|
||||||
coreProtoResolver);
|
coreProtoResolver);
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||||
|
|
||||||
@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
|||||||
String uid,
|
String uid,
|
||||||
NodeAddress makerNodeAddress,
|
NodeAddress makerNodeAddress,
|
||||||
NodeAddress takerNodeAddress,
|
NodeAddress takerNodeAddress,
|
||||||
NodeAddress arbitratorNodeAddress) {
|
NodeAddress arbitratorNodeAddress,
|
||||||
|
@Nullable String challenge) {
|
||||||
super(offer,
|
super(offer,
|
||||||
tradeAmount,
|
tradeAmount,
|
||||||
tradePrice,
|
tradePrice,
|
||||||
@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
|||||||
uid,
|
uid,
|
||||||
makerNodeAddress,
|
makerNodeAddress,
|
||||||
takerNodeAddress,
|
takerNodeAddress,
|
||||||
arbitratorNodeAddress);
|
arbitratorNodeAddress,
|
||||||
|
challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
|||||||
uid,
|
uid,
|
||||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null);
|
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||||
|
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
|
||||||
|
|
||||||
trade.setPrice(proto.getPrice());
|
trade.setPrice(proto.getPrice());
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user