Compare commits

..

3 commits

Author SHA1 Message Date
woodser
9f5a96a4d9 save multisig wallet on same thread in trade wallet operations 2024-09-12 13:35:04 -04:00
woodser
90f8c46746 save multisig wallet on same thread when initialized 2024-09-12 13:28:33 -04:00
woodser
963efe3810 check multisig state on initialization 2024-09-12 10:41:40 -04:00
501 changed files with 6734 additions and 19526 deletions

View file

@ -1,6 +1,3 @@
# GitHub Releases requires a tag, e.g:
# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1"
# git push origin 1.0.19-1
name: CI
on:
@ -14,7 +11,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-22.04, macos-13, windows-latest]
os: [ubuntu-latest, macos-13, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
@ -29,24 +26,21 @@ jobs:
cache: gradle
- name: Build with Gradle
run: ./gradlew build --stacktrace --scan
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
if: failure()
with:
name: error-reports-${{ matrix.os }}
path: ${{ github.workspace }}/desktop/build/reports
- name: cache nodes dependencies
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
include-hidden-files: true
name: cached-localnet
path: .localnet
overwrite: true
- name: Install dependencies
if: ${{ matrix.os == 'ubuntu-22.04' }}
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get update
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
sudo apt update
sudo apt install -y rpm
- name: Install WiX Toolset
if: ${{ matrix.os == 'windows-latest' }}
run: |
@ -58,116 +52,26 @@ jobs:
./gradlew clean build --refresh-keys --refresh-dependencies
./gradlew packageInstallers
working-directory: .
# get version from jar
- name: Set Version Unix
if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }}
- name: Move Release Files on Unix
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-13' }}
run: |
export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Set Version Windows
if: ${{ matrix.os == 'windows-latest' }}
run: |
$VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', ''
"VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
shell: powershell
- name: Move Release Files for Linux
if: ${{ matrix.os == 'ubuntu-22.04' }}
run: |
mkdir ${{ github.workspace }}/release-linux-rpm
mkdir ${{ github.workspace }}/release-linux-deb
mkdir ${{ github.workspace }}/release-linux-flatpak
mkdir ${{ github.workspace }}/release-linux-appimage
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb
mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak
mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256
shell: bash
- name: Move Release Files for macOS
if: ${{ matrix.os == 'macos-13' }}
run: |
mkdir ${{ github.workspace }}/release-macos
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos
cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256
mkdir ${{ github.workspace }}/release
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release
mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release
else
mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release
fi
mv desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release
shell: bash
- name: Move Release Files on Windows
if: ${{ matrix.os == 'windows-latest' }}
run: |
mkdir ${{ github.workspace }}/release-windows
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe
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
mkdir ${{ github.workspace }}/release
Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release
Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release
shell: powershell
# win
- uses: actions/upload-artifact@v4
name: "Windows artifacts"
if: ${{ matrix.os == 'windows-latest' }}
- uses: actions/upload-artifact@v3
with:
name: haveno-windows
path: ${{ github.workspace }}/release-windows
# macos
- uses: actions/upload-artifact@v4
name: "macOS artifacts"
if: ${{ matrix.os == 'macos-13' }}
with:
name: haveno-macos
path: ${{ github.workspace }}/release-macos
# linux
- uses: actions/upload-artifact@v4
name: "Linux - deb artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
with:
name: haveno-linux-deb
path: ${{ github.workspace }}/release-linux-deb
- uses: actions/upload-artifact@v4
name: "Linux - rpm artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
with:
name: haveno-linux-rpm
path: ${{ github.workspace }}/release-linux-rpm
- uses: actions/upload-artifact@v4
name: "Linux - AppImage artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
with:
name: haveno-linux-appimage
path: ${{ github.workspace }}/release-linux-appimage
- uses: actions/upload-artifact@v4
name: "Linux - flatpak artifact"
if: ${{ matrix.os == 'ubuntu-22.04' }}
with:
name: haveno-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
name: HavenoInstaller-${{ matrix.os }}
path: ${{ github.workspace }}/release

View file

@ -9,7 +9,7 @@ jobs:
build:
if: github.repository == 'haveno-dex/haveno'
name: Publish coverage
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View file

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

View file

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

3
.gitignore vendored
View file

@ -37,6 +37,3 @@ deploy
.vscode
.vim/*
*/.factorypath
.flatpak-builder
exchange.haveno.Haveno.yaml
hs_ed25519_secret_key

View file

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

View file

@ -70,11 +70,9 @@ monerod1-local:
--log-level 0 \
--add-exclusive-node 127.0.0.1:48080 \
--add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
monerod2-local:
./.localnet/monerod \
@ -90,11 +88,9 @@ monerod2-local:
--confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:58080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
monerod3-local:
./.localnet/monerod \
@ -110,11 +106,9 @@ monerod3-local:
--confirm-external-bind \
--add-exclusive-node 127.0.0.1:28080 \
--add-exclusive-node 127.0.0.1:48080 \
--max-connections-per-ip 10 \
--rpc-access-control-origins http://localhost:8080 \
--fixed-difficulty 500 \
--disable-rpc-ban \
--rpc-max-connections-per-private-ip 100 \
#--proxy 127.0.0.1:49775 \
@ -423,17 +417,6 @@ haveno-desktop-stagenet:
--apiPort=3204 \
--useNativeXmrWallet=false \
haveno-daemon-stagenet:
./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_STAGENET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=Haveno \
--apiPassword=apitest \
--apiPort=3204 \
--useNativeXmrWallet=false \
# Mainnet network
monerod:
@ -485,31 +468,6 @@ arbitrator-desktop-mainnet:
--xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \
arbitrator2-daemon-mainnet:
./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=haveno-XMR_MAINNET_arbitrator2 \
--apiPassword=apitest \
--apiPort=1205 \
--passwordRequired=false \
--xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \
arbitrator2-desktop-mainnet:
./haveno-desktop$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=haveno-XMR_MAINNET_arbitrator2 \
--apiPassword=apitest \
--apiPort=1205 \
--xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \
haveno-daemon-mainnet:
./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
@ -595,15 +553,3 @@ user3-desktop-mainnet:
--apiPort=1204 \
--useNativeXmrWallet=false \
--ignoreLocalXmrNode=false \
parner1-bot-stagenet:
./haveno-bot$(APP_EXT) \
--baseCurrencyNetwork=XMR_STAGENET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=haveno-XMR_STAGENET_user3 \
--apiPassword=apitest \
--apiPort=1204 \
--useNativeXmrWallet=false \
--ignoreLocalXmrNode=false \

View file

@ -1 +1,86 @@
The original Haveno core fork from haveno-dex/haveno adapted for the multiplatform app.
<div align="center">
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/505405b43cb74d5a996f106a3371588e)](https://app.codacy.com/gh/haveno-dex/haveno/dashboard)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=project%3Ahaveno-dex%2F2)](https://github.com/orgs/haveno-dex/projects/2) |
[![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex)
[![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
</div>
## What is Haveno?
Haveno (pronounced ha‧ve‧no) is an open source platform to exchange [Monero](https://getmonero.org) for fiat currencies like USD, EUR, and GBP or other cryptocurrencies like BTC, ETH, and BCH.
Main features:
- All communications are routed through **Tor**, to preserve your privacy
- Trades are **peer-to-peer**: trades on Haveno happen between people only, there is no central authority.
- Trades are **non-custodial**: Haveno supports arbitration in case something goes wrong during the trade, but arbitrators never have access to your funds.
- There is **No token**, because it's not needed. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
## Status of the project
Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially 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 network.
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
Main repositories:
- **[haveno](https://github.com/haveno-dex/haveno)** - This repository. The core of Haveno.
- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - The user interface.
- **[haveno-ts](https://github.com/haveno-dex/haveno-ts)** - TypeScript library for using Haveno.
- **[haveno-meta](https://github.com/haveno-dex/haveno-meta)** - For project-wide discussions and proposals.
If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties)
The PGP keys of the core team members are in `gpg_keys/`.
## Keep in touch and help out!
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`)
- Development discussions: **Haveno Development** ([#haveno-dev:monero.social](https://matrix.to/#/#haveno-dev:monero.social)) relayed on IRC/Libera (`#haveno-dev`)
Email: contact@haveno.exchange
Website: [haveno.exchange](https://haveno.exchange)
## Contributing to Haveno
See the [developer guide](docs/developer-guide.md) to get started developing for Haveno.
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.
## Bounties
To incentivize development and reward contributors we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the issues eligible for a bounty on the [dedicated Kanban board](https://github.com/orgs/haveno-dex/projects/2) or look for [issues labelled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue+is%3Aopen+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
## Support and sponsorships
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>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="150" height="150"><br>
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
</p>
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
### Bitcoin
<p>
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="150" height="150"><br>
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
</p>

View file

@ -78,7 +78,7 @@ public class ApiTestMain {
} catch (Throwable ex) {
err.println("Fault: An unexpected error occurred. " +
"Please file a report at https://github.com/haveno-dex/haveno/issues");
"Please file a report at https://haveno.exchange/issues");
ex.printStackTrace(err);
exit(EXIT_FAILURE);
}

View file

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

View file

@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};

View file

@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
"36000",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
audAccount.getId());
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
"30000.1234",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId());
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
"29500.1234",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
eurAccount.getId());
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());

View file

@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
nzdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
gbpAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
brlAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
0.0,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId(),
triggerPrice);
assertTrue(newOffer.getIsMyOffer());

View file

@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
75_000_000L,
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
50_000_000L,
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
75_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
triggerPrice);
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
50_000_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));

View file

@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
100000000000L, // exceeds amount limit
100000000000L,
"10000.0000",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
}
@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"40000.0000",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage());
@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"63000.0000",
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage());

View file

@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();

View file

@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
1_000_000L,
1_000_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesPaymentAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();

View file

@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
15_000_000L,
7_500_000L,
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 5000);

View file

@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();

View file

@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
20_000_000L,
10_500_000L,
priceMarginPctInput,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());

View file

@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
1_000_000,
1_000_000,
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("SELL offer {} created with margin based price {}.",
@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
1_000_000,
1_000_000,
0.00,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("BUY offer {} created with margin based price {}.",

View file

@ -28,7 +28,7 @@ import java.text.DecimalFormat;
import java.util.Objects;
import java.util.function.Supplier;
import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct;
import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
import static haveno.cli.CurrencyFormat.formatSatoshis;
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
@ -119,7 +119,7 @@ public class RandomOffer {
amount,
minAmount,
priceMargin,
defaultSecurityDepositPct.get(),
defaultBuyerSecurityDepositPct.get(),
"0" /*no trigger price*/);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
@ -128,7 +128,7 @@ public class RandomOffer {
amount,
minAmount,
fixedOfferPrice,
defaultSecurityDepositPct.get());
defaultBuyerSecurityDepositPct.get());
}
this.id = offer.getId();
return this;

View file

@ -1,29 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
/**
* Abstract base class for Tron-based {@link Token}s that implement the
* TRC-20 Token Standard.
*/
public abstract class Trc20Token extends Token {
public Trc20Token(String name, String tickerSymbol) {
super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}"));
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -19,9 +19,9 @@ package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class USDCoinERC20 extends Erc20Token {
public class USDCoin extends Erc20Token {
public USDCoinERC20() {
super("USD Coin", "USDC-ERC20");
public USDCoin() {
super("USD Coin", "USDC");
}
}

View file

@ -7,7 +7,3 @@ haveno.asset.coins.BitcoinCash
haveno.asset.coins.Ether
haveno.asset.coins.Litecoin
haveno.asset.coins.Monero
haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20
haveno.asset.tokens.USDCoinERC20
haveno.asset.tokens.DaiStablecoinERC20

View file

@ -32,7 +32,6 @@ public class BitcoinTest extends AbstractAssetTest {
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
}
@Test

View file

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

View file

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

View file

@ -1,47 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.bot.app;
import com.google.inject.Injector;
import haveno.core.app.misc.AppSetup;
import haveno.core.app.misc.AppSetupWithP2P;
import haveno.core.network.p2p.inventory.GetInventoryRequestHandler;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HavenoBot {
@Setter
private Injector injector;
private AppSetup appSetup;
private GetInventoryRequestHandler getInventoryRequestHandler;
public HavenoBot() {
}
public void startApplication() {
appSetup = injector.getInstance(AppSetupWithP2P.class);
appSetup.start();
getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class);
}
public void shutDown() {
getInventoryRequestHandler.shutDown();
}
}

View file

@ -1,187 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.bot.app;
import haveno.common.UserThread;
import haveno.common.app.AppModule;
import haveno.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import haveno.common.Timer;
import haveno.common.config.BaseCurrencyNetwork;
import haveno.common.config.Config;
import haveno.core.app.TorSetup;
import haveno.core.app.misc.ExecutableForAppWithP2p;
import haveno.core.app.misc.ModuleForAppWithP2p;
import haveno.core.user.Cookie;
import haveno.core.user.CookieKey;
import haveno.core.user.User;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener;
import haveno.network.p2p.peers.PeerManager;
@Slf4j
public class HavenoBotMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.1.2";
private HavenoBot bot;
private Timer checkConnectionLossTime;
public HavenoBotMain() {
super("Haveno Bot", "haveno-bot", "haveno_bot", VERSION);
}
public static void main(String[] args) {
System.out.println("HavenoBot.VERSION: " + VERSION);
new HavenoBotMain().execute(args);
}
@Override
protected int doExecute() {
super.doExecute();
checkMemory(config, this);
return keepRunning();
}
@Override
protected void addCapabilities() {
}
@Override
protected void launchApplication() {
UserThread.execute(() -> {
try {
bot = new HavenoBot();
UserThread.execute(this::onApplicationLaunched);
} catch (Exception e) {
log.error("Error launching haveno bot: {}\n", e.toString(), e);
}
});
}
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new ModuleForAppWithP2p(config);
}
@Override
protected void applyInjector() {
super.applyInjector();
bot.setInjector(injector);
}
@Override
protected void startApplication() {
Cookie cookie = injector.getInstance(User.class).getCookie();
cookie.getAsOptionalBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART).ifPresent(wasCleanTorDirSet -> {
if (wasCleanTorDirSet) {
injector.getInstance(TorSetup.class).cleanupTorFiles(() -> {
log.info("Tor directory reset");
cookie.remove(CookieKey.CLEAN_TOR_DIR_AT_RESTART);
}, log::error);
}
});
bot.startApplication();
injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
@Override
public void onDataReceived() {
// Do nothing
}
@Override
public void onNoSeedNodeAvailable() {
// Do nothing
}
@Override
public void onNoPeersAvailable() {
// Do nothing
}
@Override
public void onUpdatedDataReceived() {
// Do nothing
}
@Override
public void onTorNodeReady() {
// Do nothing
}
@Override
public void onHiddenServicePublished() {
UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
}
@Override
public void onSetupFailed(Throwable throwable) {
// Do nothing
}
@Override
public void onRequestCustomBridges() {
// Do nothing
}
});
}
private void setupConnectionLossCheck() {
// For dev testing (usually on XMR_LOCAL) we don't want to get the seed shut
// down as it is normal that the seed is the only actively running node.
if (Config.baseCurrencyNetwork() != BaseCurrencyNetwork.XMR_MAINNET) {
return;
}
if (checkConnectionLossTime != null) {
return;
}
checkConnectionLossTime = UserThread.runPeriodically(() -> {
if (injector.getInstance(PeerManager.class).getNumAllConnectionsLostEvents() > 1) {
// We set a flag to clear tor cache files at re-start. We cannot clear it now as Tor is used and
// that can cause problems.
injector.getInstance(User.class).getCookie().putAsBoolean(CookieKey.CLEAN_TOR_DIR_AT_RESTART, true);
shutDown(this);
}
}, CHECK_CONNECTION_LOSS_SEC);
}
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
bot.shutDown();
super.gracefulShutDown(resultHandler);
}
}

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="hl2" converterClass="haveno.common.app.LogHighlighter" />
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n)</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
<logger name="io.grpc.netty" level="WARN"/>
<logger name="org.apache" level="WARN" />
</configuration>

View file

@ -49,7 +49,7 @@ configure(subprojects) {
gsonVersion = '2.8.5'
guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0'
moneroJavaVersion = '0.8.36'
moneroJavaVersion = '0.8.31'
httpclient5Version = '5.0'
hamcrestVersion = '2.2'
httpclientVersion = '4.5.12'
@ -71,7 +71,7 @@ configure(subprojects) {
loggingVersion = '1.2'
lombokVersion = '1.18.30'
mockitoVersion = '5.10.0'
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11
protobufVersion = '3.19.1'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
@ -132,9 +132,7 @@ configure([project(':cli'),
project(':seednode'),
project(':statsnode'),
project(':inventory'),
project(':apitest'),
//project(':bot')
]) {
project(':apitest')]) {
apply plugin: 'application'
@ -336,7 +334,6 @@ configure(project(':p2p')) {
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "org.fxmisc.easybind:easybind:$easybindVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation "org.apache.commons:commons-lang3:$langVersion"
implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") {
exclude(module: 'slf4j-api')
}
@ -454,6 +451,123 @@ configure(project(':core')) {
mainClass = 'haveno.core.util.GenerateKeyPairs'
classpath = sourceSets.main.runtimeClasspath
}
task havenoDeps {
doLast {
// get monero binaries download url
Map moneroBinaries = [
'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release3/monero-bins-haveno-linux-x86_64.tar.gz',
'linux-x86_64-sha256' : '591e63c1e3249e0cfbba74f0302022160f64f049d06abff95417ad3ecb588581',
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release3/monero-bins-haveno-linux-aarch64.tar.gz',
'linux-aarch64-sha256' : 'fb0a91d07dbbc30646af8007205dbd11c59fb1d124a3b2d703511d8ee2739acc',
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release3/monero-bins-haveno-mac.tar.gz',
'mac-sha256' : '9eb01951976767372a3d10180c092af937afe6494928ea73e311476be5c0eba3',
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release3/monero-bins-haveno-windows.zip',
'windows-sha256' : '49b84fab3a1f69564068fecff105b6079b843d99792409dffca4a66eb279288f'
]
String osKey
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
osKey = 'windows'
} else if (Os.isFamily(Os.FAMILY_MAC)) {
osKey = 'mac'
} else {
String architecture = System.getProperty("os.arch").toLowerCase()
if (architecture.contains('aarch64') || architecture.contains('arm')) {
osKey = 'linux-aarch64'
} else {
osKey = 'linux-x86_64'
}
}
String moneroDownloadUrl = moneroBinaries[osKey]
String moneroSHA256Hash = moneroBinaries[osKey + '-sha256']
String moneroArchiveFileName = moneroDownloadUrl.tokenize('/').last()
String localnetDirName = '.localnet'
File localnetDir = new File(project.rootDir, localnetDirName)
localnetDir.mkdirs()
File moneroArchiveFile = new File(localnetDir, moneroArchiveFileName)
ext.downloadAndVerifyDependencies(moneroDownloadUrl, moneroSHA256Hash, moneroArchiveFile)
// extract if dependencies are missing or if archive was updated
File monerodFile
File moneroRpcFile
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
monerodFile = new File(localnetDir, 'monerod.exe')
moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc.exe')
} else {
monerodFile = new File(localnetDir, 'monerod')
moneroRpcFile = new File(localnetDir, 'monero-wallet-rpc')
}
if (ext.dependencyDownloadedAndVerified || !monerodFile.exists() || !moneroRpcFile.exists()) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
ext.extractArchiveZip(moneroArchiveFile, localnetDir)
} else {
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
}
// add the current platform's monero dependencies into the resources folder for installation
copy {
from "${monerodFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
copy {
from "${moneroRpcFile}"
into "${project(':core').projectDir}/src/main/resources/bin"
}
}
}
ext.extractArchiveTarGz = { File tarGzFile, File destinationDir ->
println "Extracting tar.gz ${tarGzFile}"
// Gradle's tar extraction preserves permissions (crucial for jpackage to function correctly)
copy {
from tarTree(resources.gzip(tarGzFile))
into destinationDir
}
println "Extracted to ${destinationDir}"
}
ext.extractArchiveZip = { File zipFile, File destinationDir ->
println "Extracting zip ${zipFile}..."
ant.unzip(src: zipFile, dest: destinationDir)
println "Extracted to ${destinationDir}"
}
ext.downloadAndVerifyDependencies = { String archiveURL, String archiveSHA256, File destinationArchiveFile ->
ext.dependencyDownloadedAndVerified = false
// if archive exists, check to see if its already up to date
if (destinationArchiveFile.exists()) {
println "Verifying existing archive ${destinationArchiveFile}"
ant.archiveHash = archiveSHA256
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'existingHashMatches')
if (ant.properties['existingHashMatches'] != 'true') {
println "Existing archive does not match hash ${archiveSHA256}"
} else {
println "Existing archive matches hash"
return
}
}
// download archives
println "Downloading ${archiveURL}"
ant.get(src: archiveURL, dest: destinationArchiveFile)
println 'Download saved to ' + destinationArchiveFile
// verify checksum
println 'Verifying checksum for downloaded binary ...'
ant.archiveHash = archiveSHA256
ant.checksum(file: destinationArchiveFile, algorithm: 'SHA-256', property: '${archiveHash}', verifyProperty: 'downloadedHashMatches') // use a different verifyProperty name from existing verification or it will always fail
if (ant.properties['downloadedHashMatches'] != 'true') {
ant.fail('Checksum mismatch: Downloaded archive has a different checksum than expected')
}
println 'Checksum verified'
ext.dependencyDownloadedAndVerified = true
}
}
processResources.dependsOn havenoDeps // before both test and build
}
configure(project(':cli')) {
@ -495,7 +609,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.1.2-SNAPSHOT'
version = '1.0.11-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,
@ -672,12 +786,7 @@ configure(project(':statsnode')) {
}
configure(project(':daemon')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'application'
application {
mainClass = 'haveno.daemon.app.HavenoDaemonMain'
}
mainClassName = 'haveno.daemon.app.HavenoDaemonMain'
dependencies {
implementation project(':proto')
@ -737,72 +846,6 @@ configure(project(':daemon')) {
}
}
//configure(project(':bot')) {
// apply plugin: 'com.github.johnrengelman.shadow'
// apply plugin: 'application'
//
// application {
// mainClass = 'haveno.bot.app.HavenoBotMain'
// }
//
// dependencies {
// implementation project(':proto')
// implementation project(':common')
// implementation project(':p2p')
// implementation project(':core')
// annotationProcessor "org.projectlombok:lombok:$lombokVersion"
// compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
// compileOnly "org.projectlombok:lombok:$lombokVersion"
// implementation "ch.qos.logback:logback-classic:$logbackVersion"
// implementation "ch.qos.logback:logback-core:$logbackVersion"
// implementation "com.google.code.gson:gson:$gsonVersion"
// implementation "com.google.guava:guava:$guavaVersion"
// implementation "com.google.protobuf:protobuf-java:$protobufVersion"
// implementation "org.apache.commons:commons-lang3:$langVersion"
// implementation "org.jetbrains:annotations:$jetbrainsAnnotationsVersion"
// implementation "org.slf4j:slf4j-api:$slf4jVersion"
// implementation("com.github.bisq-network:bitcoinj:$bitcoinjVersion") {
// exclude(module: 'bcprov-jdk15on')
// exclude(module: 'guava')
// exclude(module: 'jsr305')
// exclude(module: 'okhttp')
// exclude(module: 'okio')
// exclude(module: 'protobuf-java')
// exclude(module: 'slf4j-api')
// }
// implementation("com.google.inject:guice:$guiceVersion") {
// exclude(module: 'guava')
// }
// implementation("io.grpc:grpc-protobuf:$grpcVersion") {
// exclude(module: 'animal-sniffer-annotations')
// exclude(module: 'guava')
// }
// implementation("io.grpc:grpc-stub:$grpcVersion") {
// exclude(module: 'animal-sniffer-annotations')
// exclude(module: 'guava')
// }
// runtimeOnly("io.grpc:grpc-netty-shaded:$grpcVersion") {
// exclude(module: 'animal-sniffer-annotations')
// exclude(module: 'guava')
// }
// testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
// testCompileOnly "org.projectlombok:lombok:$lombokVersion"
// testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
// testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
// testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
//
// implementation("io.github.woodser:monero-java:$moneroJavaVersion") {
// exclude(module: 'jackson-core')
// exclude(module: 'jackson-annotations')
// exclude(module: 'jackson-databind')
// exclude(module: 'bcprov-jdk15on')
// exclude(group: 'org.slf4j', module: 'slf4j-simple')
// }
// implementation "org.openjfx:javafx-base:$javafxVersion:$os"
// implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
// }
//}
configure(project(':inventory')) {
apply plugin: 'com.github.johnrengelman.shadow'

View file

@ -81,7 +81,7 @@ public class OffersServiceRequest {
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMarginPct(marketPriceMarginPct)
.setSecurityDepositPct(securityDepositPct)
.setBuyerSecurityDepositPct(securityDepositPct)
.setPaymentAccountId(paymentAcctId)
.setTriggerPrice(triggerPrice)
.build();

View file

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

View file

@ -59,10 +59,8 @@ public class Capabilities {
}
public Capabilities(Collection<Capability> capabilities) {
synchronized (capabilities) {
synchronized (this.capabilities) {
this.capabilities.addAll(capabilities);
}
synchronized (this.capabilities) {
this.capabilities.addAll(capabilities);
}
}
@ -75,11 +73,9 @@ public class Capabilities {
}
public void set(Collection<Capability> capabilities) {
synchronized (capabilities) {
synchronized (this.capabilities) {
this.capabilities.clear();
this.capabilities.addAll(capabilities);
}
synchronized (this.capabilities) {
this.capabilities.clear();
this.capabilities.addAll(capabilities);
}
}
@ -91,19 +87,15 @@ public class Capabilities {
public void addAll(Capabilities capabilities) {
if (capabilities != null) {
synchronized (capabilities.capabilities) {
synchronized (this.capabilities) {
this.capabilities.addAll(capabilities.capabilities);
}
synchronized (this.capabilities) {
this.capabilities.addAll(capabilities.capabilities);
}
}
}
public boolean containsAll(final Set<Capability> requiredItems) {
synchronized(requiredItems) {
synchronized (this.capabilities) {
return capabilities.containsAll(requiredItems);
}
synchronized (this.capabilities) {
return capabilities.containsAll(requiredItems);
}
}
@ -137,9 +129,7 @@ public class Capabilities {
* @return int list of Capability ordinals
*/
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());
}
/**
@ -149,13 +139,11 @@ public class Capabilities {
* @return a {@link Capabilities} object
*/
public static Capabilities fromIntList(List<Integer> capabilities) {
synchronized (capabilities) {
return new Capabilities(capabilities.stream()
.filter(integer -> integer < Capability.values().length)
.filter(integer -> integer >= 0)
.map(integer -> Capability.values()[integer])
.collect(Collectors.toSet()));
}
return new Capabilities(capabilities.stream()
.filter(integer -> integer < Capability.values().length)
.filter(integer -> integer >= 0)
.map(integer -> Capability.values()[integer])
.collect(Collectors.toSet()));
}
/**
@ -193,9 +181,7 @@ public class Capabilities {
}
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
@ -225,10 +211,8 @@ public class Capabilities {
// 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.
public int findHighestCapability(Capabilities capabilities) {
synchronized (capabilities.capabilities) {
return (int) capabilities.capabilities.stream()
.mapToLong(e -> (long) e.ordinal())
.sum();
}
return (int) capabilities.capabilities.stream()
.mapToLong(e -> (long) e.ordinal())
.sum();
}
}

View file

@ -21,7 +21,6 @@ import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.ThresholdFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
@ -53,12 +52,11 @@ public class Log {
SizeBasedTriggeringPolicy<ILoggingEvent> triggeringPolicy = new SizeBasedTriggeringPolicy<>();
triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB"));
triggeringPolicy.setContext(loggerContext);
triggeringPolicy.start();
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg%n");
encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n");
encoder.start();
appender.setEncoder(encoder);
@ -66,43 +64,25 @@ public class Log {
appender.setTriggeringPolicy(triggeringPolicy);
appender.start();
// log errors in separate file
PatternLayoutEncoder errorEncoder = new PatternLayoutEncoder();
errorEncoder.setContext(loggerContext);
errorEncoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger: %msg%n%ex");
errorEncoder.start();
logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logbackLogger.addAppender(appender);
logbackLogger.setLevel(Level.INFO);
RollingFileAppender<ILoggingEvent> errorAppender = new RollingFileAppender<>();
errorAppender.setEncoder(errorEncoder);
// log errors in separate file
// not working as expected still.... damn logback...
/* FileAppender errorAppender = new FileAppender();
errorAppender.setEncoder(encoder);
errorAppender.setName("Error");
errorAppender.setContext(loggerContext);
errorAppender.setFile(fileName + "_error.log");
FixedWindowRollingPolicy errorRollingPolicy = new FixedWindowRollingPolicy();
errorRollingPolicy.setContext(loggerContext);
errorRollingPolicy.setParent(errorAppender);
errorRollingPolicy.setFileNamePattern(fileName + "_error_%i.log");
errorRollingPolicy.setMinIndex(1);
errorRollingPolicy.setMaxIndex(20);
errorRollingPolicy.start();
SizeBasedTriggeringPolicy<ILoggingEvent> errorTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
errorTriggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB"));
errorTriggeringPolicy.start();
ThresholdFilter thresholdFilter = new ThresholdFilter();
thresholdFilter.setLevel("WARN");
thresholdFilter.start();
errorAppender.setRollingPolicy(errorRollingPolicy);
errorAppender.setTriggeringPolicy(errorTriggeringPolicy);
errorAppender.addFilter(thresholdFilter);
LevelFilter levelFilter = new LevelFilter();
levelFilter.setLevel(Level.ERROR);
levelFilter.setOnMatch(FilterReply.ACCEPT);
levelFilter.setOnMismatch(FilterReply.DENY);
levelFilter.start();
errorAppender.addFilter(levelFilter);
errorAppender.start();
logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
logbackLogger.addAppender(errorAppender);
logbackLogger.addAppender(appender);
logbackLogger.setLevel(Level.INFO);
logbackLogger.addAppender(errorAppender);*/
}
public static void setCustomLogLevel(String pattern, Level logLevel) {

View file

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.1.2";
public static final String VERSION = "1.0.11";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
@ -72,25 +72,6 @@ public class Version {
return false;
}
public static int compare(String version1, String version2) {
if (version1.equals(version2))
return 0;
else if (getMajorVersion(version1) > getMajorVersion(version2))
return 1;
else if (getMajorVersion(version1) < getMajorVersion(version2))
return -1;
else if (getMinorVersion(version1) > getMinorVersion(version2))
return 1;
else if (getMinorVersion(version1) < getMinorVersion(version2))
return -1;
else if (getPatchVersion(version1) > getPatchVersion(version2))
return 1;
else if (getPatchVersion(version1) < getPatchVersion(version2))
return -1;
else
return 0;
}
private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
@ -99,7 +80,7 @@ public class Version {
// The version no. for the objects sent over the network. A change will break the serialization of old objects.
// If objects are used for both network and database the network version is applied.
public static final String P2P_NETWORK_VERSION = System.getenv().getOrDefault("P2P_NETWORK_VERSION", "X");
public static final String P2P_NETWORK_VERSION = "A";
// The version no. of the serialized data stored to disc. A change will break the serialization of old objects.
// VERSION = 0.5.0 -> LOCAL_DB_VERSION = 1
@ -110,9 +91,8 @@ 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 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app.
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2;
// VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
public static final int TRADE_PROTOCOL_VERSION = 1;
private static String p2pMessageVersion;
public static String getP2PMessageVersion() {

View file

@ -117,8 +117,6 @@ public class Config {
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String PASSWORD_REQUIRED = "passwordRequired";
public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@ -206,8 +204,6 @@ public class Config {
public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation;
public final boolean passwordRequired;
public final boolean updateXmrBinaries;
public final String xmrBlockchainPath;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@ -625,20 +621,6 @@ public class Config {
.ofType(boolean.class)
.defaultsTo(false);
ArgumentAcceptingOptionSpec<Boolean> updateXmrBinariesOpt =
parser.accepts(UPDATE_XMR_BINARIES,
"Update Monero binaries if applicable")
.withRequiredArg()
.ofType(boolean.class)
.defaultsTo(true);
ArgumentAcceptingOptionSpec<String> xmrBlockchainPathOpt =
parser.accepts(XMR_BLOCKCHAIN_PATH,
"Path to Monero blockchain when using local Monero node")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("");
try {
CompositeOptionSet options = new CompositeOptionSet();
@ -751,8 +733,6 @@ public class Config {
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.passwordRequired = options.valueOf(passwordRequiredOpt);
this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
@ -762,11 +742,11 @@ public class Config {
}
// Create all appDataDir subdirectories and assign to their respective properties
File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
this.storageDir = mkdir(xmrNetworkDir, "db");
this.torDir = mkdir(xmrNetworkDir, "tor");
this.walletDir = mkdir(xmrNetworkDir, "wallet");
File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
this.keyStorageDir = mkdir(btcNetworkDir, "keys");
this.storageDir = mkdir(btcNetworkDir, "db");
this.torDir = mkdir(btcNetworkDir, "tor");
this.walletDir = mkdir(btcNetworkDir, "wallet");
// Assign values to special-case static fields
APP_DATA_DIR_VALUE = appDataDir;

View file

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

View file

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

View file

@ -32,7 +32,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
@ -69,28 +68,24 @@ public class FileUtil {
pruneBackup(backupFileDir, numMaxBackupFiles);
} catch (IOException e) {
log.error("Backup key failed: {}\n", e.getMessage(), e);
log.error("Backup key failed: " + e.getMessage());
e.printStackTrace();
}
}
}
}
public static List<File> getBackupFiles(File dir, String fileName) {
public static File getLatestBackupFile(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
if (!backupDir.exists()) return new ArrayList<File>();
if (!backupDir.exists()) return null;
String dirName = "backups_" + fileName;
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
if (!backupFileDir.exists()) return new ArrayList<File>();
if (!backupFileDir.exists()) return null;
File[] files = backupFileDir.listFiles();
return Arrays.asList(files);
}
public static File getLatestBackupFile(File dir, String fileName) {
List<File> files = getBackupFiles(dir, fileName);
if (files.isEmpty()) return null;
files.sort(Comparator.comparing(File::getName));
return files.get(files.size() - 1);
if (files == null || files.length == 0) return null;
Arrays.sort(files, Comparator.comparing(File::getName));
return files[files.length - 1];
}
public static void deleteRollingBackup(File dir, String fileName) {
@ -102,7 +97,7 @@ public class FileUtil {
try {
FileUtils.deleteDirectory(backupFileDir);
} catch (IOException e) {
log.error("Delete backup key failed: {}\n", e.getMessage(), e);
e.printStackTrace();
}
}
@ -178,7 +173,8 @@ public class FileUtil {
}
}
} catch (Throwable t) {
log.error("Could not delete file, error={}\n", t.getMessage(), t);
log.error(t.toString());
t.printStackTrace();
throw new IOException(t);
}
}

View file

@ -69,7 +69,11 @@ public class CommonSetup {
"The system tray is not supported on the current platform.".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else {
log.error("Uncaught Exception from thread {}, error={}\n", Thread.currentThread().getName(), throwable.getMessage(), throwable);
log.error("Uncaught Exception from thread " + Thread.currentThread().getName());
log.error("throwableMessage= " + throwable.getMessage());
log.error("throwableClass= " + throwable.getClass());
log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable));
throwable.printStackTrace();
UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false));
}
};
@ -109,7 +113,8 @@ public class CommonSetup {
if (!pathOfCodeSource.endsWith("classes"))
log.info("Path to Haveno jar file: " + pathOfCodeSource);
} catch (URISyntaxException e) {
log.error(ExceptionUtils.getStackTrace(e));
log.error(e.toString());
e.printStackTrace();
}
}
}

View file

@ -25,8 +25,6 @@ import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.exception.ExceptionUtils;
@Slf4j
public class TaskRunner<T extends Model> {
private final Queue<Class<? extends Task<T>>> tasks = new LinkedBlockingQueue<>();
@ -69,8 +67,8 @@ public class TaskRunner<T extends Model> {
log.info("Run task: " + currentTask.getSimpleName());
currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run();
} catch (Throwable throwable) {
log.error(ExceptionUtils.getStackTrace(throwable));
handleErrorMessage("Error at taskRunner, error=" + throwable.getMessage());
throwable.printStackTrace();
handleErrorMessage("Error at taskRunner: " + throwable.getMessage());
}
} else {
resultHandler.handleResult();

View file

@ -11,8 +11,8 @@
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with Bisq. If not, see <http://www.gnu.org/licenses/>.
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.common.util;
@ -25,67 +25,38 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* Utility class for creating single-threaded executors.
*/
public class SingleThreadExecutorUtils {
private SingleThreadExecutorUtils() {
// Prevent instantiation
}
public static ExecutorService getSingleThreadExecutor(Class<?> aClass) {
validateClass(aClass);
return getSingleThreadExecutor(aClass.getSimpleName());
String name = aClass.getSimpleName();
return getSingleThreadExecutor(name);
}
public static ExecutorService getNonDaemonSingleThreadExecutor(Class<?> aClass) {
validateClass(aClass);
return getSingleThreadExecutor(aClass.getSimpleName(), false);
String name = aClass.getSimpleName();
return getSingleThreadExecutor(name, false);
}
public static ExecutorService getSingleThreadExecutor(String name) {
validateName(name);
return getSingleThreadExecutor(name, true);
}
public static ListeningExecutorService getSingleThreadListeningExecutor(String name) {
validateName(name);
return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name));
}
public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) {
validateThreadFactory(threadFactory);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) {
ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
final ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) {
return new ThreadFactoryBuilder()
.setNameFormat(name + "-%d")
.setNameFormat(name)
.setDaemon(isDaemonThread)
.build();
}
private static void validateClass(Class<?> aClass) {
if (aClass == null) {
throw new IllegalArgumentException("Class must not be null.");
}
}
private static void validateName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must not be null or empty.");
}
}
private static void validateThreadFactory(ThreadFactory threadFactory) {
if (threadFactory == null) {
throw new IllegalArgumentException("ThreadFactory must not be null.");
}
}
}

View file

@ -331,7 +331,8 @@ public class Utilities {
clipboard.setContent(clipboardContent);
}
} catch (Throwable e) {
log.error("copyToClipboard failed: {}\n", e.getMessage(), e);
log.error("copyToClipboard failed " + e.getMessage());
e.printStackTrace();
}
}

View file

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

View file

@ -40,7 +40,6 @@ import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferRestrictions;
import haveno.core.payment.ChargeBackRisk;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.TradeLimits;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.support.dispute.Dispute;
@ -499,15 +498,10 @@ public class AccountAgeWitnessService {
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
}
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) {
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) {
if (paymentAccount == null)
return 0;
if (buyerAsTakerWithoutDeposit) {
TradeLimits tradeLimits = new TradeLimits();
return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
}
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
if (hasTradeLimitException(accountAgeWitness)) {
@ -743,13 +737,14 @@ public class AccountAgeWitnessService {
}
public Optional<SignedWitness> traderSignAndPublishPeersAccountAgeWitness(Trade trade) {
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
BigInteger tradeAmount = trade.getAmount();
checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring");
PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey();
checkNotNull(peersPubKey, "Peers pub key must not be null");
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString());
BigInteger tradeAmount = trade.getAmount();
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}",
trade.toString());
checkNotNull(tradeAmount, "Trade amount must not be null");
checkNotNull(peersPubKey, "Peers pub key must not be null");
try {
return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey);

View file

@ -97,13 +97,25 @@ public class AlertManager {
}
protected List<String> getPubKeyList() {
return List.of(
"0326b14f3a55d02575dceed5202b8b125f458cbe0fdceeee294b443bf1a8d8cf78",
"03d62d14438adbe7aea688ade1f73933c6f0a705f238c02c5b54b83dd1e4fca225",
"023c8fdea9ff2d03daef54337907e70a7b0e20084a75fcc3ad2f0c28d8b691dea1"
);
if (useDevPrivilegeKeys) return List.of(DevEnv.DEV_PRIVILEGE_PUB_KEY);
switch (Config.baseCurrencyNetwork()) {
case XMR_LOCAL:
return List.of(
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
case XMR_STAGENET:
return List.of(
"036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f",
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854",
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36");
case XMR_MAINNET:
return List.of();
default:
throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork());
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -96,11 +96,22 @@ public class PrivateNotificationManager implements MessageListener {
}
protected List<String> getPubKeyList() {
return List.of(
"0326b14f3a55d02575dceed5202b8b125f458cbe0fdceeee294b443bf1a8d8cf78",
"03d62d14438adbe7aea688ade1f73933c6f0a705f238c02c5b54b83dd1e4fca225",
"023c8fdea9ff2d03daef54337907e70a7b0e20084a75fcc3ad2f0c28d8b691dea1"
);
if (useDevPrivilegeKeys) return List.of(DevEnv.DEV_PRIVILEGE_PUB_KEY);
switch (Config.baseCurrencyNetwork()) {
case XMR_LOCAL:
return List.of(
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492");
case XMR_STAGENET:
return List.of(
"02ba7c5de295adfe57b60029f3637a2c6b1d0e969a8aaefb9e0ddc3a7963f26925",
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854",
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36");
case XMR_MAINNET:
return List.of();
default:
throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork());
}
}
private void handleMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, NodeAddress senderNodeAddress) {

View file

@ -239,18 +239,14 @@ public class CoreApi {
xmrConnectionService.stopCheckingConnection();
}
public MoneroRpcConnection getBestXmrConnection() {
return xmrConnectionService.getBestConnection();
public MoneroRpcConnection getBestAvailableXmrConnection() {
return xmrConnectionService.getBestAvailableConnection();
}
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
xmrConnectionService.setAutoSwitch(autoSwitch);
}
public boolean getXmrConnectionAutoSwitch() {
return xmrConnectionService.getAutoSwitch();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Monero node
///////////////////////////////////////////////////////////////////////////////////////////
@ -264,11 +260,11 @@ public class CoreApi {
}
public void startXmrNode(XmrNodeSettings settings) throws IOException {
xmrLocalNode.start(settings);
xmrLocalNode.startNode(settings);
}
public void stopXmrNode() {
xmrLocalNode.stop();
xmrLocalNode.stopNode();
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -413,22 +409,18 @@ public class CoreApi {
}
public void postOffer(String currencyCode,
String directionAsString,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
String directionAsString,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double buyerSecurityDeposit,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreOffersService.postOffer(currencyCode,
directionAsString,
priceAsString,
@ -436,14 +428,10 @@ public class CoreApi {
marketPriceMargin,
amountAsLong,
minAmountAsLong,
securityDepositPct,
buyerSecurityDeposit,
triggerPriceAsString,
reserveExactAmount,
paymentAccountId,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo,
sourceOfferId,
resultHandler,
errorMessageHandler);
}
@ -456,11 +444,8 @@ public class CoreApi {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
double buyerSecurityDeposit,
PaymentAccount paymentAccount) {
return coreOffersService.editOffer(offerId,
currencyCode,
direction,
@ -469,11 +454,8 @@ public class CoreApi {
marketPriceMargin,
amount,
minAmount,
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
buyerSecurityDeposit,
paymentAccount);
}
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -549,11 +531,9 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
long amountAsLong,
String challenge,
Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId);
offer.setChallenge(challenge);
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
}

View file

@ -52,9 +52,6 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.exception.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
@ -62,12 +59,11 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CoreDisputesService {
// TODO: persist in DisputeResult?
public enum PayoutSuggestion {
public enum DisputePayout {
BUYER_GETS_TRADE_AMOUNT,
BUYER_GETS_ALL,
BUYER_GETS_ALL, // used in desktop
SELLER_GETS_TRADE_AMOUNT,
SELLER_GETS_ALL,
SELLER_GETS_ALL, // used in desktop
CUSTOM
}
@ -173,17 +169,17 @@ public class CoreDisputesService {
// create dispute result
var closeDate = new Date();
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
PayoutSuggestion payoutSuggestion;
DisputePayout payout;
if (customWinnerAmount > 0) {
payoutSuggestion = PayoutSuggestion.CUSTOM;
payout = DisputePayout.CUSTOM;
} else if (winner == DisputeResult.Winner.BUYER) {
payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
} else if (winner == DisputeResult.Winner.SELLER) {
payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
} else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
}
applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
// close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
@ -208,7 +204,7 @@ public class CoreDisputesService {
throw new IllegalStateException(errMessage, err);
});
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
e.printStackTrace();
throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage());
}
}
@ -228,26 +224,26 @@ public class CoreDisputesService {
* 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.
*/
public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
Contract contract = dispute.getContract();
Trade trade = tradeManager.getTrade(dispute.getTradeId());
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
BigInteger tradeAmount = contract.getTradeAmount();
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) {
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
} else if (payout == DisputePayout.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
} else if (payout == DisputePayout.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
} else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
} else if (payout == DisputePayout.CUSTOM) {
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");

View file

@ -3,12 +3,7 @@ package haveno.core.api;
import com.google.inject.Singleton;
import haveno.core.api.model.TradeInfo;
import haveno.core.support.messages.ChatMessage;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.SellerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.Trade.Phase;
import haveno.proto.grpc.NotificationMessage;
import haveno.proto.grpc.NotificationMessage.NotificationType;
import java.util.Iterator;
@ -51,18 +46,7 @@ public class CoreNotificationService {
.build());
}
public void sendTradeNotification(Trade trade, Phase phase, String title, String message) {
// play chime when maker's trade is taken
if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound();
// play chime when buyer can confirm payment sent
if (trade instanceof BuyerTrade && phase == Trade.Phase.DEPOSITS_UNLOCKED) HavenoUtils.playChimeSound();
// play chime when seller sees buyer confirm payment sent
if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound();
// send notification
public void sendTradeNotification(Trade trade, String title, String message) {
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.TRADE_UPDATE)
.setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage())
@ -73,7 +57,6 @@ public class CoreNotificationService {
}
public void sendChatNotification(ChatMessage chatMessage) {
HavenoUtils.playChimeSound();
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.CHAT_MESSAGE)
.setTimestamp(System.currentTimeMillis())

View file

@ -43,7 +43,6 @@ import static haveno.common.util.MathUtils.exactMultiply;
import static haveno.common.util.MathUtils.roundDoubleToLong;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.monetary.CryptoMoney;
import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney;
@ -67,7 +66,9 @@ import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import static java.util.Comparator.comparing;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -123,6 +124,7 @@ public class CoreOffersService {
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
})
.collect(Collectors.toList());
offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers;
}
@ -141,9 +143,12 @@ public class CoreOffersService {
}
List<OpenOffer> getMyOffers() {
return openOfferManager.getOpenOffers().stream()
List<OpenOffer> offers = openOfferManager.getOpenOffers().stream()
.filter(o -> o.getOffer().isMyOffer(keyRing))
.collect(Collectors.toList());
Set<Offer> offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images
Set<String> offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet());
return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList());
};
List<OpenOffer> getMyOffers(String direction, String currencyCode) {
@ -154,7 +159,7 @@ public class CoreOffersService {
}
OpenOffer getMyOffer(String id) {
return openOfferManager.getOpenOffer(id)
return openOfferManager.getOpenOfferById(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
@ -167,38 +172,19 @@ public class CoreOffersService {
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double securityDepositPct,
double securityDeposit,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo,
String sourceOfferId,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
if (paymentAccount == null)
throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
// clone offer if sourceOfferId given
if (!sourceOfferId.isEmpty()) {
cloneOffer(sourceOfferId,
currencyCode,
priceAsString,
useMarketBasedPrice,
marketPriceMargin,
triggerPriceAsString,
paymentAccountId,
extraInfo,
resultHandler,
errorMessageHandler);
return;
}
// create new offer
String upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId();
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
@ -213,78 +199,22 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
securityDeposit,
paymentAccount);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// We don't support atm funding from external wallet to keep it simple.
boolean useSavingsWallet = true;
//noinspection ConstantConditions
placeOffer(offer,
triggerPriceAsString,
true,
useSavingsWallet,
reserveExactAmount,
null,
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
private void cloneOffer(String sourceOfferId,
String currencyCode,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
String triggerPriceAsString,
String paymentAccountId,
String extraInfo,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
// get source offer
OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId);
Offer sourceOffer = sourceOpenOffer.getOffer();
// get trade currency (default source currency)
if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode();
if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode();
String upperCaseCurrencyCode = currencyCode.toUpperCase();
// get price (default source price)
Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString);
if (price == null) useMarketBasedPrice = true;
// get payment account
if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
// get extra info
if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
// create cloned offer
Offer offer = createOfferService.createClonedOffer(sourceOffer,
upperCaseCurrencyCode,
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
paymentAccount,
extraInfo);
// verify cloned offer
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
// place offer
placeOffer(offer,
triggerPriceAsString,
true,
false, // ignored when cloning
sourceOfferId,
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
// TODO: this implementation is missing; implement.
Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
@ -293,11 +223,8 @@ public class CoreOffersService {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
double buyerSecurityDeposit,
PaymentAccount paymentAccount) {
return createOfferService.createAndGetOffer(offerId,
direction,
currencyCode.toUpperCase(),
@ -306,11 +233,8 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
buyerSecurityDeposit,
paymentAccount);
}
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -320,6 +244,26 @@ public class CoreOffersService {
// -------------------------- PRIVATE HELPERS -----------------------------
private Set<Offer> getOffersWithDuplicateKeyImages(List<Offer> offers) {
Set<Offer> duplicateFundedOffers = new HashSet<Offer>();
Set<String> seenKeyImages = new HashSet<String>();
for (Offer offer : offers) {
if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) {
if (offer == offer2) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
duplicateFundedOffers.add(offer2);
}
}
}
}
}
return duplicateFundedOffers;
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
@ -333,7 +277,6 @@ public class CoreOffersService {
String triggerPriceAsString,
boolean useSavingsWallet,
boolean reserveExactAmount,
String sourceOfferId,
Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
@ -341,8 +284,6 @@ public class CoreOffersService {
useSavingsWallet,
triggerPriceAsLong,
reserveExactAmount,
true,
sourceOfferId,
resultHandler::accept,
errorMessageHandler);
}

View file

@ -64,14 +64,9 @@ class CorePaymentAccountsService {
}
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
validateFormFields(form);
PaymentAccount paymentAccount = form.toPaymentAccount();
setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload
verifyPaymentAccountHasRequiredFields(paymentAccount);
if (paymentAccount instanceof CryptoCurrencyAccount) {
CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount;
verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress());
}
user.addPaymentAccountIfNotExists(paymentAccount);
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Saved payment account with id {} and payment method {}.",
@ -171,12 +166,6 @@ class CorePaymentAccountsService {
.collect(Collectors.toList());
}
private void validateFormFields(PaymentAccountForm form) {
for (PaymentAccountFormField field : form.getFields()) {
validateFormField(form, field.getId(), field.getValue());
}
}
void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
// get payment method id

View file

@ -72,7 +72,7 @@ class CorePriceService {
* @return Price per 1 XMR in the given currency (traditional or crypto)
*/
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
}

View file

@ -47,6 +47,7 @@ import haveno.core.support.messages.ChatMessage;
import haveno.core.support.traderchat.TradeChatSession;
import haveno.core.support.traderchat.TraderChatManager;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Tradable;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeUtil;
@ -54,6 +55,9 @@ import haveno.core.trade.protocol.BuyerProtocol;
import haveno.core.trade.protocol.SellerProtocol;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.util.validation.BtcAddressValidator;
import haveno.core.xmr.model.AddressEntry;
import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT;
import haveno.core.xmr.wallet.BtcWalletService;
import static java.lang.String.format;
import java.math.BigInteger;
@ -62,8 +66,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bitcoinj.core.Coin;
@Singleton
@Slf4j
@ -79,6 +82,7 @@ class CoreTradesService {
private final TakeOfferModel takeOfferModel;
private final TradeManager tradeManager;
private final TraderChatManager traderChatManager;
private final TradeUtil tradeUtil;
private final OfferUtil offerUtil;
private final User user;
@ -100,6 +104,7 @@ class CoreTradesService {
this.takeOfferModel = takeOfferModel;
this.tradeManager = tradeManager;
this.traderChatManager = traderChatManager;
this.tradeUtil = tradeUtil;
this.offerUtil = offerUtil;
this.user = user;
}
@ -125,7 +130,7 @@ class CoreTradesService {
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
String currencyCode = offer.getCurrencyCode();
OfferDirection direction = offer.getOfferPayload().getDirection();
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
if (offer.getPrice() != null) {
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
@ -156,7 +161,7 @@ class CoreTradesService {
errorMessageHandler
);
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
e.printStackTrace();
errorMessageHandler.handleErrorMessage(e.getMessage());
}
}
@ -199,7 +204,7 @@ class CoreTradesService {
String getTradeRole(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return TradeUtil.getRole(getTrade(tradeId));
return tradeUtil.getRole(getTrade(tradeId));
}
Trade getTrade(String tradeId) {
@ -216,7 +221,8 @@ class CoreTradesService {
}
private Optional<Trade> getClosedTrade(String tradeId) {
return closedTradableManager.getTradeById(tradeId);
Optional<Tradable> tradable = closedTradableManager.getTradeById(tradeId);
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
}
List<Trade> getTrades() {
@ -259,9 +265,40 @@ class CoreTradesService {
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
}
private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
// TODO This and identical logic should be refactored into TradeUtil.
try {
return btcWalletService.getFeeEstimationTransaction(fromAddress,
toAddress,
amount,
TRADE_PAYOUT).getFee();
} catch (Exception ex) {
log.error("", ex);
throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
}
}
// Throws a RuntimeException trade is already closed.
private void verifyTradeIsNotClosed(String tradeId) {
if (getClosedTrade(tradeId).isPresent())
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
}
// Throws a RuntimeException if address is not valid.
private void verifyIsValidBTCAddress(String address) {
try {
new BtcAddressValidator().validate(address);
} catch (Throwable t) {
log.error("", t);
throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
}
}
// Throws a RuntimeException if address has a zero balance.
private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
if (fromAddressBalance.isZero())
throw new IllegalStateException(format("funds already withdrawn from address '%s'",
fromAddressEntry.getAddressString()));
}
}

View file

@ -32,7 +32,6 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener;
@ -41,9 +40,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.stream.Collectors;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
@ -64,6 +61,7 @@ import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemonRpc;
import monero.daemon.model.MoneroDaemonInfo;
import monero.daemon.model.MoneroPeer;
@Slf4j
@Singleton
@ -72,14 +70,6 @@ public final class XmrConnectionService {
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
public enum XmrConnectionFallbackType {
LOCAL,
CUSTOM,
PROVIDED
}
private final Object lock = new Object();
private final Object pollLock = new Object();
@ -92,14 +82,12 @@ public final class XmrConnectionService {
private final XmrLocalNode xmrLocalNode;
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
private final ObjectProperty<List<MoneroRpcConnection>> connections = new SimpleObjectProperty<>();
private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
private final ObjectProperty<List<MoneroPeer>> peers = 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 DownloadListener downloadListener = new DownloadListener();
@Getter
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0);
private Socks5ProxyProvider socks5ProxyProvider;
@ -110,7 +98,6 @@ public final class XmrConnectionService {
private Boolean isConnected = false;
@Getter
private MoneroDaemonInfo lastInfo;
private Long lastFallbackInvocation;
private Long lastLogPollErrorTimestamp;
private long lastLogDaemonNotSyncedTimestamp;
private Long syncStartHeight;
@ -119,7 +106,6 @@ public final class XmrConnectionService {
@Getter
private boolean isShutDownStarted;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
private XmrKeyImagePoller keyImagePoller;
// connection switching
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
@ -128,9 +114,6 @@ public final class XmrConnectionService {
private int numRequestsLastMinute;
private long lastSwitchTimestamp;
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
private boolean fallbackApplied;
private boolean usedSyncingLocalNodeBeforeStartup;
@Inject
public XmrConnectionService(P2PService p2PService,
@ -158,13 +141,7 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override
public void onTorNodeReady() {
ThreadUtils.submitToPool(() -> {
try {
initialize();
} catch (Exception e) {
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
}
});
initialize();
}
@Override
public void onHiddenServicePublished() {}
@ -270,29 +247,18 @@ public final class XmrConnectionService {
updatePolling();
}
public MoneroRpcConnection getBestConnection() {
return getBestConnection(new ArrayList<MoneroRpcConnection>());
public MoneroRpcConnection getBestAvailableConnection() {
accountService.checkAccountOpen();
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
addLocalNodeIfIgnored(ignoredConnections);
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
}
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) {
accountService.checkAccountOpen();
// user needs to authorize fallback on startup after using locally synced node
if (fallbackRequiredBeforeConnectionSwitch()) {
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
return null;
}
// get best connection
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet);
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());
return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0]));
}
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
@ -304,7 +270,7 @@ public final class XmrConnectionService {
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
return;
}
MoneroRpcConnection bestConnection = getBestConnection();
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
}
@ -355,7 +321,7 @@ public final class XmrConnectionService {
if (currentConnection != null) excludedConnections.add(currentConnection);
// get connection to switch to
MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections);
// remove from excluded connections after period
UserThread.runAfter(() -> {
@ -363,7 +329,7 @@ public final class XmrConnectionService {
}, EXCLUDE_CONNECTION_SECONDS);
// return if no connection to switch to
if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
if (bestConnection == null) {
log.warn("No connection to switch to");
return false;
}
@ -380,11 +346,6 @@ public final class XmrConnectionService {
connectionList.setAutoSwitch(autoSwitch);
}
public boolean getAutoSwitch() {
accountService.checkAccountOpen();
return connectionList.getAutoSwitch();
}
public boolean isConnectionLocalHost() {
return isConnectionLocalHost(getConnection());
}
@ -419,25 +380,14 @@ public final class XmrConnectionService {
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
}
public XmrKeyImagePoller getKeyImagePoller() {
synchronized (lock) {
if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
return keyImagePoller;
}
}
private long getKeyImageRefreshPeriodMs() {
return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
// ----------------------------- APP METHODS ------------------------------
public ReadOnlyIntegerProperty numConnectionsProperty() {
return numConnections;
public ReadOnlyIntegerProperty numPeersProperty() {
return numPeers;
}
public ReadOnlyObjectProperty<List<MoneroRpcConnection>> connectionsProperty() {
return connections;
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
return peers;
}
public ReadOnlyObjectProperty<MoneroRpcConnection> connectionProperty() {
@ -445,7 +395,7 @@ public final class XmrConnectionService {
}
public boolean hasSufficientPeersForBroadcast() {
return numConnections.get() >= getMinBroadcastConnections();
return numPeers.get() >= getMinBroadcastConnections();
}
public LongProperty chainHeightProperty() {
@ -468,24 +418,6 @@ public final class XmrConnectionService {
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 --------------------------------
private void doneDownload() {
@ -520,13 +452,6 @@ public final class XmrConnectionService {
private void initialize() {
// initialize key image poller
getKeyImagePoller();
new Thread(() -> {
HavenoUtils.waitFor(20000);
keyImagePoller.poll(); // TODO: keep or remove first poll?s
}).start();
// initialize connections
initializeConnections();
@ -539,7 +464,7 @@ public final class XmrConnectionService {
log.info(getClass() + ".onAccountOpened() called");
initialize();
} catch (Exception e) {
log.error("Error initializing connection service after account opened, error={}\n", e.getMessage(), e);
e.printStackTrace();
throw new RuntimeException(e);
}
}
@ -566,7 +491,7 @@ public final class XmrConnectionService {
xmrLocalNode.addListener(new XmrLocalNodeListener() {
@Override
public void onNodeStarted(MoneroDaemonRpc daemon) {
log.info("Local monero node started, height={}", daemon.getHeight());
log.info("Local monero node started");
}
@Override
@ -593,13 +518,8 @@ public final class XmrConnectionService {
// update connection
if (isConnected) {
setConnection(connection.getUri());
// reset error connecting to local node
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
connectionServiceFallbackType.set(null);
}
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestConnection();
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
}
}
@ -607,7 +527,7 @@ public final class XmrConnectionService {
}
// restore connections
if (!isFixedConnection()) {
if ("".equals(config.xmrNode)) {
// load previous or default connections
if (coreContext.isApiUser()) {
@ -619,10 +539,8 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
if (node.hasClearNetAddress()) {
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);
}
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -634,10 +552,8 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress()) {
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection);
}
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection);
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -647,17 +563,15 @@ public final class XmrConnectionService {
}
// restore last connection
if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
if (isFixedConnection()) {
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())) {
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
log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@ -666,11 +580,14 @@ public final class XmrConnectionService {
// restore auto switch
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
else connectionManager.setAutoSwitch(true);
// start local node if applicable
maybeStartLocalNode();
// update connection
if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
MoneroRpcConnection bestConnection = getBestConnection();
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
}
} else if (!isInitialized) {
@ -680,6 +597,9 @@ public final class XmrConnectionService {
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
connectionManager.setConnection(connection);
// start local node if applicable
maybeStartLocalNode();
}
// register connection listener
@ -688,26 +608,31 @@ public final class XmrConnectionService {
}
// notify initial connection
lastRefreshPeriodMs = getRefreshPeriodMs();
onConnectionChanged(connectionManager.getConnection());
}
public void startLocalNode() throws Exception {
// cannot start local node as seed node
if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node");
}
private void maybeStartLocalNode() {
// start local node
log.info("Starting local node");
xmrLocalNode.start();
// skip if seed node
if (HavenoUtils.isSeedNode()) return;
// start local node if offline and used as last connection
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
try {
log.info("Starting local node");
xmrLocalNode.startMoneroNode();
} catch (Exception e) {
log.warn("Unable to start local monero node: " + e.getMessage());
e.printStackTrace();
}
}
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return;
if (currentConnection == null) {
log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
log.warn("Setting daemon connection to null");
Thread.dumpStack();
}
synchronized (lock) {
if (currentConnection == null) {
@ -728,10 +653,6 @@ public final class XmrConnectionService {
numUpdates.set(numUpdates.get() + 1);
});
}
// update key image poller
keyImagePoller.setDaemon(getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling
doPollDaemon();
@ -781,56 +702,37 @@ public final class XmrConnectionService {
try {
// poll daemon
if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
if (daemon == null) switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
try {
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo();
} catch (Exception e) {
// skip handling if shutting down
if (isShutDownStarted) return;
// invoke fallback handling on startup error
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (lastInfo == null && canFallback) {
if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
lastFallbackInvocation = System.currentTimeMillis();
if (usedSyncingLocalNodeBeforeStartup) {
log.warn("Failed to fetch 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 {
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
}
}
// fallback to provided nodes if custom connection fails on startup
if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) {
log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage());
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
initializeConnections();
return;
}
// log error message periodically
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage());
if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e));
log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage());
if (DevEnv.isDevMode()) e.printStackTrace();
lastLogPollErrorTimestamp = System.currentTimeMillis();
}
// switch to best connection
switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = daemon.getInfo(); // caught internally if still fails
}
// connected to daemon
isConnected = true;
connectionServiceFallbackType.set(null);
// determine if blockchain is syncing locally
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
// write sync status to preferences
preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing);
// throttle warnings if daemon not synced
if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) {
@ -862,15 +764,16 @@ public final class XmrConnectionService {
downloadListener.progress(percent, blocksLeft, null);
}
// set available connections
List<MoneroRpcConnection> availableConnections = new ArrayList<>();
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) {
availableConnections.add(connection);
}
}
connections.set(availableConnections);
numConnections.set(availableConnections.size());
// set peer connections
// TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections
// try {
// peers.set(getOnlinePeers());
// } catch (Exception err) {
// // TODO: peers unknown due to restricted RPC call
// }
// numPeers.set(peers.get().size());
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
peers.set(new ArrayList<MoneroPeer>());
// notify update
numUpdates.set(numUpdates.get() + 1);
@ -900,15 +803,13 @@ public final class XmrConnectionService {
}
}
private List<MoneroPeer> getOnlinePeers() {
return daemon.getPeers().stream()
.filter(peer -> peer.isOnline())
.collect(Collectors.toList());
}
private boolean isFixedConnection() {
return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
}
private boolean isCustomConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
}
private boolean isProvidedConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
}
}

View file

@ -25,8 +25,6 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
import haveno.core.xmr.XmrNodeSettings;
import haveno.core.xmr.nodes.XmrNodes;
import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.wallet.XmrWalletService;
import java.io.File;
@ -57,7 +55,6 @@ public class XmrLocalNode {
private MoneroConnectionManager connectionManager;
private final Config config;
private final Preferences preferences;
private final XmrNodes xmrNodes;
private final List<XmrLocalNodeListener> listeners = new ArrayList<>();
// required arguments
@ -72,12 +69,9 @@ public class XmrLocalNode {
}
@Inject
public XmrLocalNode(Config config,
Preferences preferences,
XmrNodes xmrNodes) {
public XmrLocalNode(Config config, Preferences preferences) {
this.config = config;
this.preferences = preferences;
this.xmrNodes = xmrNodes;
this.daemon = new MoneroDaemonRpc(getUri());
// initialize connection manager to listen to local connection
@ -107,20 +101,7 @@ public class XmrLocalNode {
* Returns whether Haveno should ignore a local Monero node even if it is usable.
*/
public boolean shouldBeIgnored() {
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;
return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
}
public void addListener(XmrLocalNodeListener listener) {
@ -139,11 +120,7 @@ public class XmrLocalNode {
}
public boolean equalsUri(String uri) {
try {
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
} catch (Exception e) {
return false;
}
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
}
/**
@ -173,45 +150,33 @@ public class XmrLocalNode {
/**
* Start a local Monero node from settings.
*/
public void start() throws IOException {
public void startMoneroNode() throws IOException {
var settings = preferences.getXmrNodeSettings();
this.start(settings);
this.startNode(settings);
}
/**
* Start local Monero node. Throws MoneroError if the node cannot be started.
* Persist the settings to preferences if the node started successfully.
*/
public void start(XmrNodeSettings settings) throws IOException {
public void startNode(XmrNodeSettings settings) throws IOException {
if (isDetected()) throw new IllegalStateException("Local Monero node already online");
log.info("Starting local Monero node: " + settings);
var args = new ArrayList<>(MONEROD_ARGS);
var dataDir = "";
if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR;
}
} else {
dataDir = config.xmrBlockchainPath; // startup config overrides settings
}
if (dataDir != null && !dataDir.isEmpty()) {
args.add("--data-dir=" + dataDir);
var dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR;
}
if (dataDir != null) args.add("--data-dir=" + dataDir);
var bootstrapUrl = settings.getBootstrapUrl();
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
args.add("--bootstrap-daemon-address=" + bootstrapUrl);
}
var syncBlockchain = settings.getSyncBlockchain();
if (syncBlockchain != null && !syncBlockchain) {
args.add("--no-sync");
}
var flags = settings.getStartupFlags();
if (flags != null) {
args.addAll(flags);
@ -226,7 +191,7 @@ public class XmrLocalNode {
* Stop the current local Monero node if we own its process.
* Does not remove the last XmrNodeSettings.
*/
public void stop() {
public void stopNode() {
if (!isDetected()) throw new IllegalStateException("Local Monero node is not running");
if (daemon.getProcess() == null || !daemon.getProcess().isAlive()) throw new IllegalStateException("Cannot stop local Monero node because we don't own its process"); // TODO (woodser): remove isAlive() check after monero-java 0.5.4 which nullifies internal process
daemon.stopProcess();

View file

@ -78,9 +78,6 @@ public class OfferInfo implements Payload {
@Nullable
private final String splitOutputTxHash;
private final long splitOutputTxFee;
private final boolean isPrivateOffer;
private final String challenge;
private final String extraInfo;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId();
@ -114,9 +111,6 @@ public class OfferInfo implements Payload {
this.arbitratorSigner = builder.getArbitratorSigner();
this.splitOutputTxHash = builder.getSplitOutputTxHash();
this.splitOutputTxFee = builder.getSplitOutputTxFee();
this.isPrivateOffer = builder.isPrivateOffer();
this.challenge = builder.getChallenge();
this.extraInfo = builder.getExtraInfo();
}
public static OfferInfo toOfferInfo(Offer offer) {
@ -143,7 +137,6 @@ public class OfferInfo implements Payload {
.withIsActivated(isActivated)
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
.withChallenge(openOffer.getChallenge())
.build();
}
@ -184,10 +177,7 @@ public class OfferInfo implements Payload {
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
.withVersionNumber(offer.getOfferPayload().getVersionNr())
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
.withIsPrivateOffer(offer.isPrivateOffer())
.withChallenge(offer.getChallenge())
.withExtraInfo(offer.getCombinedExtraInfo());
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -225,12 +215,9 @@ public class OfferInfo implements Payload {
.setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion)
.setSplitOutputTxFee(splitOutputTxFee)
.setIsPrivateOffer(isPrivateOffer);
.setSplitOutputTxFee(splitOutputTxFee);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return builder.build();
}
@ -268,9 +255,6 @@ public class OfferInfo implements Payload {
.withArbitratorSigner(proto.getArbitratorSigner())
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
.withIsPrivateOffer(proto.getIsPrivateOffer())
.withChallenge(proto.getChallenge())
.withExtraInfo(proto.getExtraInfo())
.build();
}
}

View file

@ -77,8 +77,7 @@ public final class PaymentAccountForm implements PersistablePayload {
AUSTRALIA_PAYID,
CASH_APP,
PAYPAL,
VENMO,
PAYSAFE;
VENMO;
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());

View file

@ -172,14 +172,14 @@ public class TradeInfo implements Payload {
.withAmount(trade.getAmount().longValueExact())
.withMakerFee(trade.getMakerFee().longValueExact())
.withTakerFee(trade.getTakerFee().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
.withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade))

View file

@ -98,7 +98,7 @@ public class XmrBalanceInfo implements Payload {
public String toString() {
return "XmrBalanceInfo{" +
"balance=" + balance +
", unlockedBalance=" + availableBalance +
"unlockedBalance=" + availableBalance +
", lockedBalance=" + pendingBalance +
", reservedOfferBalance=" + reservedOfferBalance +
", reservedTradeBalance=" + reservedTradeBalance +

View file

@ -63,9 +63,6 @@ public final class OfferInfoBuilder {
private String arbitratorSigner;
private String splitOutputTxHash;
private long splitOutputTxFee;
private boolean isPrivateOffer;
private String challenge;
private String extraInfo;
public OfferInfoBuilder withId(String id) {
this.id = id;
@ -237,21 +234,6 @@ public final class OfferInfoBuilder {
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() {
return new OfferInfo(this);
}

View file

@ -73,7 +73,7 @@ public class AppStartupState {
isWalletSynced.set(true);
});
xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> {
xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
if (xmrConnectionService.hasSufficientPeersForBroadcast())
hasSufficientPeersForBroadcast.set(true);
});

View file

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

View file

@ -82,8 +82,6 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class HavenoExecutable implements GracefulShutDownHandler, HavenoSetup.HavenoSetupListener, UncaughtExceptionHandler {
// TODO: regular expression is used to parse application name for the flatpak manifest, a more stable approach would be nice
// Don't edit the next line unless you're only editing in between the quotes.
public static final String DEFAULT_APP_NAME = "Haveno";
public static final int EXIT_SUCCESS = 0;
@ -100,7 +98,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
protected AppModule module;
protected Config config;
@Getter
protected boolean isShutDownStarted;
protected boolean isShutdownInProgress;
private boolean isReadOnly;
private Thread keepRunningThread;
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
@ -126,7 +124,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
System.exit(EXIT_FAILURE);
} catch (Throwable ex) {
System.err.println("fault: An unexpected error occurred. " +
"Please file a report at https://github.com/haveno-dex/haveno/issues");
"Please file a report at https://haveno.exchange/issues");
ex.printStackTrace(System.err);
System.exit(EXIT_FAILURE);
}
@ -203,7 +201,8 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
startApplication();
}
} catch (InterruptedException | ExecutionException e) {
log.error("An error occurred: {}\n", e.getMessage(), e);
log.error("An error occurred: {}", e.getMessage());
e.printStackTrace();
}
});
}
@ -330,12 +329,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
// ignore if shut down started
if (isShutDownStarted) {
log.info("Ignoring call to gracefulShutDown, already started");
// ignore if shut down in progress
if (isShutdownInProgress) {
log.info("Ignoring call to gracefulShutDown, already in progress");
return;
}
isShutDownStarted = true;
isShutdownInProgress = true;
ResultHandler resultHandler;
if (shutdownCompletedHandler != null) {
@ -357,49 +356,49 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// notify trade protocols and wallets to prepare for shut down before shutting down
Set<Runnable> tasks = new HashSet<Runnable>();
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
try {
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
} catch (Exception e) {
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e);
e.printStackTrace();
}
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager
log.info("Shutting down OpenOfferManager");
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
// listen for shut down of wallets setup
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
// shut down offer book service
injector.getInstance(OfferBookService.class).shutDown();
// shut down p2p service
log.info("Shutting down P2P service");
injector.getInstance(P2PService.class).shutDown(() -> {
// shut down p2p service
injector.getInstance(P2PService.class).shutDown(() -> {
// shut down monero wallets and connections
log.info("Shutting down wallet and connection services");
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
// done shutting down
log.info("Graceful shutdown completed. Exiting now.");
module.close(injector);
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
});
injector.getInstance(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown();
});
// shut down trade and wallet services
log.info("Shutting down trade and wallet services");
injector.getInstance(OfferBookService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(BtcWalletService.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown();
injector.getInstance(XmrConnectionService.class).shutDown();
injector.getInstance(WalletsSetup.class).shutDown();
});
} catch (Throwable t) {
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
log.error("App shutdown failed with exception {}", t.toString());
t.printStackTrace();
completeShutdown(resultHandler, EXIT_FAILURE, systemExit);
}
}

View file

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

View file

@ -53,9 +53,7 @@ import haveno.core.alert.Alert;
import haveno.core.alert.AlertManager;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService;
import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager;
@ -133,10 +131,7 @@ public class HavenoSetup {
private final Preferences preferences;
private final User user;
private final AlertManager alertManager;
@Getter
private final Config config;
@Getter
private final CoreContext coreContext;
private final AccountAgeWitnessService accountAgeWitnessService;
private final TorSetup torSetup;
private final CoinFormatter formatter;
@ -159,9 +154,6 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler;
@Setter
@Nullable
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter
@Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler;
@Setter
@Nullable
@ -177,7 +169,7 @@ public class HavenoSetup {
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
@Setter
@Nullable
private Runnable showPopupIfInvalidXmrConfigHandler;
private Runnable showPopupIfInvalidBtcConfigHandler;
@Setter
@Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@ -236,7 +228,6 @@ public class HavenoSetup {
User user,
AlertManager alertManager,
Config config,
CoreContext coreContext,
AccountAgeWitnessService accountAgeWitnessService,
TorSetup torSetup,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
@ -262,7 +253,6 @@ public class HavenoSetup {
this.user = user;
this.alertManager = alertManager;
this.config = config;
this.coreContext = coreContext;
this.accountAgeWitnessService = accountAgeWitnessService;
this.torSetup = torSetup;
this.formatter = formatter;
@ -273,7 +263,6 @@ public class HavenoSetup {
this.arbitrationManager = arbitrationManager;
HavenoUtils.havenoSetup = this;
HavenoUtils.preferences = preferences;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -370,7 +359,7 @@ public class HavenoSetup {
// install monerod
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) {
if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) {
log.info("Installing monerod");
monerodFile.getParentFile().mkdirs();
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
@ -380,14 +369,15 @@ public class HavenoSetup {
// install monero-wallet-rpc
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) {
if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) {
log.info("Installing monero-wallet-rpc");
moneroWalletRpcFile.getParentFile().mkdirs();
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
moneroWalletRpcFile.setExecutable(true);
}
} catch (Exception e) {
log.warn("Failed to install Monero binaries: {}\n", e.getMessage(), e);
e.printStackTrace();
log.warn("Failed to install Monero binaries: " + e.toString());
}
}
@ -430,12 +420,6 @@ public class HavenoSetup {
getXmrDaemonSyncProgress().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");
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
@ -462,7 +446,7 @@ public class HavenoSetup {
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidXmrConfigHandler,
showPopupIfInvalidBtcConfigHandler,
() -> {},
() -> {});
}
@ -735,10 +719,6 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg();
}
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceFallbackType();
}
public StringProperty getTopErrorMsg() {
return topErrorMsg;
}

View file

@ -87,7 +87,7 @@ public class P2PNetworkSetup {
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
String result;
int p2pPeers = (int) numP2pPeers;

View file

@ -28,9 +28,6 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import javax.annotation.Nullable;
import org.apache.commons.lang3.exception.ExceptionUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -51,7 +48,8 @@ public class TorSetup {
if (resultHandler != null)
resultHandler.run();
} catch (IOException e) {
log.error(ExceptionUtils.getStackTrace(e));
e.printStackTrace();
log.error(e.toString());
if (errorMessageHandler != null)
errorMessageHandler.handleErrorMessage(e.toString());
}

View file

@ -117,10 +117,10 @@ public class WalletAppSetup {
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
@Nullable Runnable showPopupIfInvalidXmrConfigHandler,
@Nullable Runnable showPopupIfInvalidBtcConfigHandler,
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion());
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
xmrInfoBinding = EasyBind.combine(
@ -199,8 +199,8 @@ public class WalletAppSetup {
walletInitializedHandler.run();
},
exception -> {
if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
showPopupIfInvalidXmrConfigHandler.run();
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
showPopupIfInvalidBtcConfigHandler.run();
} else {
walletServiceException.set(exception);
}

View file

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

View file

@ -114,11 +114,11 @@ public class FilterManager {
this.providersRepository = providersRepository;
this.ignoreDevMsg = ignoreDevMsg;
publicKeys = List.of(
"0326b14f3a55d02575dceed5202b8b125f458cbe0fdceeee294b443bf1a8d8cf78",
"03d62d14438adbe7aea688ade1f73933c6f0a705f238c02c5b54b83dd1e4fca225",
"023c8fdea9ff2d03daef54337907e70a7b0e20084a75fcc3ad2f0c28d8b691dea1"
);
publicKeys = useDevPrivilegeKeys ?
Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) :
List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27",
"029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f",
"034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9");
banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork);
}
@ -406,10 +406,6 @@ public class FilterManager {
.anyMatch(e -> e.equals(address));
}
public String getDisableTradeBelowVersion() {
return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
}
public boolean requireUpdateToNewVersionForTrading() {
if (getFilter() == null) {
return false;

View file

@ -19,8 +19,10 @@ package haveno.core.locale;
import com.google.protobuf.Message;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
public final class CryptoCurrency extends TradeCurrency {
// http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618
private final static String PREFIX = "";
@ -54,7 +56,7 @@ public final class CryptoCurrency extends TradeCurrency {
public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) {
return new CryptoCurrency(proto.getCode(),
CurrencyUtil.getNameByCode(proto.getCode()),
proto.getName(),
proto.getCryptoCurrency().getIsAsset());
}

View file

@ -66,13 +66,21 @@ import static java.lang.String.format;
@Slf4j
public class CurrencyUtil {
public static void setup() {
setBaseCurrencyCode(baseCurrencyCode);
setBaseCurrencyCode("XMR");
}
private static final AssetRegistry assetRegistry = new AssetRegistry();
private static String baseCurrencyCode = "XMR";
private static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
return Arrays.asList(
new TraditionalCurrency("XAG", "Silver"),
new TraditionalCurrency("XAU", "Gold"),
new TraditionalCurrency("XGB", "Goldback")
);
}
// Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results.
// The main improvement was already achieved with using memoize for the source maps, but
// the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50%
@ -116,14 +124,6 @@ public class CurrencyUtil {
return new ArrayList<>(traditionalCurrencyMapSupplier.get().values());
}
public static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
return Arrays.asList(
new TraditionalCurrency("XAG", "Silver"),
new TraditionalCurrency("XAU", "Gold"),
new TraditionalCurrency("XGB", "Goldback")
);
}
public static Collection<TraditionalCurrency> getAllSortedTraditionalCurrencies(Comparator comparator) {
return (List<TraditionalCurrency>) getAllSortedTraditionalCurrencies().stream()
.sorted(comparator)
@ -200,10 +200,6 @@ public class CurrencyUtil {
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("ETH", "Ether"));
result.add(new CryptoCurrency("LTC", "Litecoin"));
result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin"));
result.add(new CryptoCurrency("USDT-ERC20", "Tether USD"));
result.add(new CryptoCurrency("USDT-TRC20", "Tether USD"));
result.add(new CryptoCurrency("USDC-ERC20", "USD Coin"));
result.sort(TradeCurrency::compareTo);
return result;
}
@ -299,9 +295,6 @@ public class CurrencyUtil {
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
}
if (isCryptoCurrencyCodeBase(currencyCode)) {
return true;
}
boolean isCryptoCurrency;
if (currencyCode == null) {
@ -328,21 +321,6 @@ public class CurrencyUtil {
return isCryptoCurrency;
}
private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return false;
currencyCode = currencyCode.toUpperCase();
return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
}
public static String getCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return null;
currencyCode = currencyCode.toUpperCase();
if (currencyCode.contains("USDT")) return "USDT";
if (currencyCode.contains("USDC")) return "USDC";
if (currencyCode.contains("DAI")) return "DAI";
return currencyCode;
}
public static Optional<CryptoCurrency> getCryptoCurrency(String currencyCode) {
return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode));
}
@ -406,13 +384,6 @@ public class CurrencyUtil {
removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na");
return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset);
}
if (isTraditionalNonFiatCurrency(currencyCode)) {
return getTraditionalNonFiatCurrencies().stream()
.filter(currency -> currency.getCode().equals(currencyCode))
.findAny()
.map(TradeCurrency::getName)
.orElse(currencyCode);
}
try {
return Currency.getInstance(currencyCode).getDisplayName();
} catch (Throwable t) {

View file

@ -19,16 +19,19 @@ package haveno.core.locale;
import haveno.common.proto.ProtobufferRuntimeException;
import haveno.common.proto.persistable.PersistablePayload;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@EqualsAndHashCode
@ToString
@Getter
@Slf4j
public abstract class TradeCurrency implements PersistablePayload, Comparable<TradeCurrency> {
protected final String code;
@EqualsAndHashCode.Exclude
protected final String name;
public TradeCurrency(String code, String name) {
@ -79,23 +82,4 @@ public abstract class TradeCurrency implements PersistablePayload, Comparable<Tr
return this.name.compareTo(other.name);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof TradeCurrency) {
TradeCurrency other = (TradeCurrency) obj;
return code.equals(other.code);
}
return false;
}
@Override
public int hashCode() {
return code.hashCode();
}
}

View file

@ -36,12 +36,14 @@ package haveno.core.locale;
import com.google.protobuf.Message;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.util.Currency;
import java.util.Locale;
@EqualsAndHashCode(callSuper = true)
@ToString
@Getter
public final class TraditionalCurrency extends TradeCurrency {
@ -86,7 +88,7 @@ public final class TraditionalCurrency extends TradeCurrency {
}
public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) {
return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode()));
return new TraditionalCurrency(proto.getCode(), proto.getName());
}

View file

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

View file

@ -33,8 +33,10 @@ import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
@ -92,7 +94,6 @@ public class CreateOfferService {
Version.VERSION.replace(".", "");
}
// TODO: add trigger price?
public Offer createAndGetOffer(String offerId,
OfferDirection direction,
String currencyCode,
@ -101,12 +102,10 @@ public class CreateOfferService {
Price fixedPrice,
boolean useMarketBasedPrice,
double marketPriceMargin,
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
String extraInfo) {
log.info("Create and get offer with offerId={}, " +
double securityDepositAsDouble,
PaymentAccount paymentAccount) {
log.info("create and get offer with offerId={}, " +
"currencyCode={}, " +
"direction={}, " +
"fixedPrice={}, " +
@ -114,10 +113,7 @@ public class CreateOfferService {
"marketPriceMargin={}, " +
"amount={}, " +
"minAmount={}, " +
"securityDepositPct={}, " +
"isPrivateOffer={}, " +
"buyerAsTakerWithoutDeposit={}, " +
"extraInfo={}",
"securityDeposit={}",
offerId,
currencyCode,
direction,
@ -126,21 +122,7 @@ public class CreateOfferService {
marketPriceMargin,
amount,
minAmount,
securityDepositPct,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
extraInfo);
// must nullify empty string so contracts match
if ("".equals(extraInfo)) extraInfo = null;
// verify config for private no deposit offers
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
if (buyerAsTakerWithoutDeposit || isPrivateOffer) {
if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit");
if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers");
if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit");
}
securityDepositAsDouble);
// verify fixed price xor market price with margin
if (fixedPrice != null) {
@ -148,29 +130,25 @@ public class CreateOfferService {
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
}
// verify price
long creationTime = new Date().getTime();
NodeAddress makerAddress = p2PService.getAddress();
boolean useMarketBasedPriceValue = fixedPrice == null &&
useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) &&
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
// verify price
if (fixedPrice == null && !useMarketBasedPriceValue) {
throw new IllegalArgumentException("Must provide fixed price");
}
// adjust amount and min amount
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
// generate one-time challenge for private offer
String challenge = null;
String challengeHash = null;
if (isPrivateOffer) {
challenge = HavenoUtils.generateChallenge();
challengeHash = HavenoUtils.getChallengeHash(challenge);
// adjust amount and min amount for fixed-price offer
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
if (fixedPrice != null) {
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
}
long creationTime = new Date().getTime();
NodeAddress makerAddress = p2PService.getAddress();
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
@ -183,16 +161,21 @@ public class CreateOfferService {
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
// reserved for future use cases
// Use null values if not set
boolean isPrivateOffer = false;
boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0;
long upperClosePrice = 0;
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
String hashOfChallenge = null;
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
currencyCode,
direction);
offerUtil.validateOfferData(
securityDepositPct,
securityDepositAsDouble,
paymentAccount,
currencyCode);
@ -206,11 +189,11 @@ public class CreateOfferService {
useMarketBasedPriceValue,
amountAsLong,
minAmountAsLong,
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.MAKER_FEE_PCT,
HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.PENALTY_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
securityDepositPct,
securityDepositAsDouble,
securityDepositAsDouble,
baseCurrencyCode,
counterCurrencyCode,
paymentAccount.getPaymentMethod().getId(),
@ -228,110 +211,44 @@ public class CreateOfferService {
upperClosePrice,
lowerClosePrice,
isPrivateOffer,
challengeHash,
hashOfChallenge,
extraDataMap,
Version.TRADE_PROTOCOL_VERSION,
null,
null,
null,
extraInfo);
null);
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
offer.setChallenge(challenge);
return offer;
}
// TODO: add trigger price?
public Offer createClonedOffer(Offer sourceOffer,
String currencyCode,
Price fixedPrice,
boolean useMarketBasedPrice,
double marketPriceMargin,
PaymentAccount paymentAccount,
String extraInfo) {
log.info("Cloning offer with sourceId={}, " +
"currencyCode={}, " +
"fixedPrice={}, " +
"useMarketBasedPrice={}, " +
"marketPriceMargin={}, " +
"extraInfo={}",
sourceOffer.getId(),
currencyCode,
fixedPrice == null ? null : fixedPrice.getValue(),
useMarketBasedPrice,
marketPriceMargin,
extraInfo);
public BigInteger getReservedFundsForOffer(OfferDirection direction,
BigInteger amount,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
String newOfferId = OfferUtil.getRandomOfferId();
Offer editedOffer = createAndGetOffer(newOfferId,
sourceOfferPayload.getDirection(),
currencyCode,
BigInteger.valueOf(sourceOfferPayload.getAmount()),
BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
fixedPrice,
useMarketBasedPrice,
marketPriceMargin,
sourceOfferPayload.getSellerSecurityDepositPct(),
paymentAccount,
sourceOfferPayload.isPrivateOffer(),
sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
extraInfo);
BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
amount,
buyerSecurityDeposit,
sellerSecurityDeposit);
if (!offerUtil.isBuyOffer(direction))
reservedFundsForOffer = reservedFundsForOffer.add(amount);
// generate one-time challenge for private offer
String challenge = null;
String challengeHash = null;
if (sourceOfferPayload.isPrivateOffer()) {
challenge = HavenoUtils.generateChallenge();
challengeHash = HavenoUtils.getChallengeHash(challenge);
}
OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
long date = new Date().getTime();
OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
date,
sourceOfferPayload.getOwnerNodeAddress(),
sourceOfferPayload.getPubKeyRing(),
sourceOfferPayload.getDirection(),
editedOfferPayload.getPrice(),
editedOfferPayload.getMarketPriceMarginPct(),
editedOfferPayload.isUseMarketBasedPrice(),
sourceOfferPayload.getAmount(),
sourceOfferPayload.getMinAmount(),
sourceOfferPayload.getMakerFeePct(),
sourceOfferPayload.getTakerFeePct(),
sourceOfferPayload.getPenaltyFeePct(),
sourceOfferPayload.getBuyerSecurityDepositPct(),
sourceOfferPayload.getSellerSecurityDepositPct(),
editedOfferPayload.getBaseCurrencyCode(),
editedOfferPayload.getCounterCurrencyCode(),
editedOfferPayload.getPaymentMethodId(),
editedOfferPayload.getMakerPaymentAccountId(),
editedOfferPayload.getCountryCode(),
editedOfferPayload.getAcceptedCountryCodes(),
editedOfferPayload.getBankId(),
editedOfferPayload.getAcceptedBankIds(),
editedOfferPayload.getVersionNr(),
sourceOfferPayload.getBlockHeightAtOfferCreation(),
editedOfferPayload.getMaxTradeLimit(),
editedOfferPayload.getMaxTradePeriod(),
sourceOfferPayload.isUseAutoClose(),
sourceOfferPayload.isUseReOpenAfterAutoClose(),
sourceOfferPayload.getLowerClosePrice(),
sourceOfferPayload.getUpperClosePrice(),
sourceOfferPayload.isPrivateOffer(),
challengeHash,
editedOfferPayload.getExtraDataMap(),
sourceOfferPayload.getProtocolVersion(),
null,
null,
sourceOfferPayload.getReserveTxKeyImages(),
editedOfferPayload.getExtraInfo());
Offer clonedOffer = new Offer(clonedOfferPayload);
clonedOffer.setPriceFeedService(priceFeedService);
clonedOffer.setChallenge(challenge);
clonedOffer.setState(Offer.State.AVAILABLE);
return clonedOffer;
return reservedFundsForOffer;
}
public BigInteger getSecurityDeposit(OfferDirection direction,
BigInteger amount,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
return offerUtil.isBuyOffer(direction) ?
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
}
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
Restrictions.getSellerSecurityDepositAsPercent();
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -342,4 +259,26 @@ public class CreateOfferService {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
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);
}
}

View file

@ -18,6 +18,7 @@
package haveno.core.offer;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler;
@ -114,12 +115,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Setter
transient private boolean isReservedFundsSpent;
@JsonExclude
@Getter
@Setter
@Nullable
transient private String challenge;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -280,7 +275,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
}
public void setErrorMessage(String errorMessage) {
errorMessageProperty.set(errorMessage);
UserThread.await(() -> errorMessageProperty.set(errorMessage));
}
@ -342,18 +337,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
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() {
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
}
@ -420,23 +403,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
return "";
}
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() {
public String getExtraInfo() {
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
@ -447,8 +414,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO);
else
return "";
}

View file

@ -36,9 +36,6 @@ package haveno.core.offer;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.file.JsonFileManager;
@ -48,51 +45,45 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import haveno.network.utils.Utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import monero.daemon.model.MoneroKeyImageSpentStatus;
/**
* Handles validation and announcement of offers added or removed.
* Handles storage and retrieval of offers.
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
*/
@Slf4j
public class OfferBookService {
private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
private final P2PService p2PService;
private final PriceFeedService priceFeedService;
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
private final FilterManager filterManager;
private final JsonFileManager jsonFileManager;
private final XmrConnectionService xmrConnectionService;
private final List<Offer> validOffers = new ArrayList<Offer>();
private final List<Offer> invalidOffers = new ArrayList<Offer>();
private final Map<String, Timer> invalidOfferTimers = new HashMap<>();
// poll key images of offers
private XmrKeyImagePoller keyImagePoller;
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
public interface OfferBookChangedListener {
void onAdded(Offer offer);
void onRemoved(Offer offer);
}
@ -113,59 +104,51 @@ public class OfferBookService {
this.xmrConnectionService = xmrConnectionService;
jsonFileManager = new JsonFileManager(storageDir);
// listen for connection changes to monerod
xmrConnectionService.addConnectionListener((connection) -> {
maybeInitializeKeyImagePoller();
keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
});
// listen for offers
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
ThreadUtils.execute(() -> {
UserThread.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
synchronized (validOffers) {
try {
validateOfferPayload(offerPayload);
replaceValidOffer(offer);
announceOfferAdded(offer);
} catch (IllegalArgumentException e) {
// ignore illegal offers
} catch (RuntimeException e) {
replaceInvalidOffer(offer); // offer can become valid later
}
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
}
}
});
}, OfferBookService.class.getSimpleName());
});
}
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
ThreadUtils.execute(() -> {
UserThread.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
removeValidOffer(offerPayload.getId());
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
announceOfferRemoved(offer);
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
}
});
}, OfferBookService.class.getSimpleName());
});
}
});
@ -188,16 +171,6 @@ 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);
}
}
});
}
@ -205,10 +178,6 @@ public class OfferBookService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public boolean hasOffer(String offerId) {
return hasValidOffer(offerId);
}
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (filterManager.requireUpdateToNewVersionForTrading()) {
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
@ -264,9 +233,16 @@ public class OfferBookService {
}
public List<Offer> getOffers() {
synchronized (validOffers) {
return new ArrayList<>(validOffers);
}
return p2PService.getDataMap().values().stream()
.filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload)
.map(data -> {
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
return offer;
})
.collect(Collectors.toList());
}
public List<Offer> getOffersByCurrency(String direction, String currencyCode) {
@ -290,7 +266,7 @@ public class OfferBookService {
}
public void shutDown() {
xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
if (keyImagePoller != null) keyImagePoller.clearKeyImages();
}
@ -298,131 +274,37 @@ public class OfferBookService {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void announceOfferAdded(Offer offer) {
xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
}
}
private synchronized void maybeInitializeKeyImagePoller() {
if (keyImagePoller != null) return;
keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
private void announceOfferRemoved(Offer offer) {
updateReservedFundsSpentStatus(offer);
removeKeyImages(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
}
private boolean hasValidOffer(String offerId) {
for (Offer offer : getOffers()) {
if (offer.getId().equals(offerId)) {
return true;
// handle when key images spent
keyImagePoller.addListener(new XmrKeyImageListener() {
@Override
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses) {
UserThread.execute(() -> {
for (String keyImage : spentStatuses.keySet()) {
updateAffectedOffers(keyImage);
}
});
}
}
return false;
}
private void replaceValidOffer(Offer offer) {
synchronized (validOffers) {
removeValidOffer(offer.getId());
validOffers.add(offer);
}
});
// first poll after 20s
// TODO: remove?
new Thread(() -> {
HavenoUtils.waitFor(20000);
keyImagePoller.poll();
}).start();
}
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 long getKeyImageRefreshPeriodMs() {
return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
}
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) {
for (Offer offer : getOffers()) {
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
listener.onRemoved(offer);
@ -433,9 +315,10 @@ public class OfferBookService {
}
}
private void updateReservedFundsSpentStatus(Offer offer) {
private void setReservedFundsSpent(Offer offer) {
if (keyImagePoller == null) return;
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
offer.setReservedFundsSpent(true);
}
}

View file

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

View file

@ -102,7 +102,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo";
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
@ -157,9 +156,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
private final boolean isPrivateOffer;
@Nullable
private final String challengeHash;
@Nullable
private final String extraInfo;
private final String hashOfChallenge;
///////////////////////////////////////////////////////////////////////////////////////////
@ -198,13 +195,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
long lowerClosePrice,
long upperClosePrice,
boolean isPrivateOffer,
@Nullable String challengeHash,
@Nullable String hashOfChallenge,
@Nullable Map<String, String> extraDataMap,
int protocolVersion,
@Nullable NodeAddress arbitratorSigner,
@Nullable byte[] arbitratorSignature,
@Nullable List<String> reserveTxKeyImages,
@Nullable String extraInfo) {
@Nullable List<String> reserveTxKeyImages) {
this.id = id;
this.date = date;
this.ownerNodeAddress = ownerNodeAddress;
@ -242,8 +238,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.lowerClosePrice = lowerClosePrice;
this.upperClosePrice = upperClosePrice;
this.isPrivateOffer = isPrivateOffer;
this.challengeHash = challengeHash;
this.extraInfo = extraInfo;
this.hashOfChallenge = hashOfChallenge;
}
public byte[] getHash() {
@ -289,13 +284,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
lowerClosePrice,
upperClosePrice,
isPrivateOffer,
challengeHash,
hashOfChallenge,
extraDataMap,
protocolVersion,
arbitratorSigner,
null,
reserveTxKeyImages,
null
reserveTxKeyImages
);
return signee.getHash();
@ -334,21 +328,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
boolean isBuyerTaker = getDirection() == OfferDirection.SELL;
if (isPrivateOffer() && isBuyerTaker) {
return securityDepositUnadjusted;
} else {
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
}
return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted);
}
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
}
public boolean isBuyerAsTakerWithoutDeposit() {
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -391,12 +376,11 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash);
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
}
@ -408,6 +392,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap();
@ -443,13 +428,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getLowerClosePrice(),
proto.getUpperClosePrice(),
proto.getIsPrivateOffer(),
ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
hashOfChallenge,
extraDataMapMap,
proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
reserveTxKeyImages,
ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
reserveTxKeyImages);
}
@Override
@ -491,15 +475,14 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer +
",\r\n challengeHash='" + challengeHash +
",\r\n hashOfChallenge='" + hashOfChallenge + '\'' +
",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\r\n extraInfo='" + extraInfo +
"\r\n} ";
}
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
// The json is used for the hash in the contract and change of order would cause a different hash and
// The json is used for the hash in the contract and change of oder would cause a different hash and
// therefore a failure during trade.
public static class JsonSerializer implements com.google.gson.JsonSerializer<OfferPayload> {
@Override
@ -536,8 +519,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
object.add("extraInfo", context.serialize(offerPayload.getExtraInfo()));
// reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts
return object;
}
}

View file

@ -37,7 +37,6 @@ import haveno.core.monetary.Volume;
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CAPABILITIES;
import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.F2F_CITY;
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
@ -49,7 +48,6 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CashAppAccount;
import haveno.core.payment.CashAtAtmAccount;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount;
@ -60,8 +58,8 @@ import haveno.core.trade.statistics.ReferralIdService;
import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
import haveno.network.p2p.P2PService;
import java.math.BigInteger;
import java.util.HashMap;
@ -122,10 +120,9 @@ public class OfferUtil {
public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
OfferDirection direction) {
return paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
: 0;
}
@ -219,10 +216,6 @@ public class OfferUtil {
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
}
if (paymentAccount instanceof CashAtAtmAccount) {
extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo());
}
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
@ -235,16 +228,16 @@ public class OfferUtil {
return extraDataMap.isEmpty() ? null : extraDataMap;
}
public void validateOfferData(double securityDeposit,
public void validateOfferData(double buyerSecurityDeposit,
PaymentAccount paymentAccount,
String currencyCode) {
checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
"securityDeposit must not exceed " +
getMaxSecurityDepositAsPercent());
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
getMaxBuyerSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
"securityDeposit must not be less than " +
getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),

View file

@ -48,7 +48,6 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@EqualsAndHashCode
public final class OpenOffer implements Tradable {
@ -97,9 +96,6 @@ public final class OpenOffer implements Tradable {
@Getter
private String reserveTxKey;
@Getter
@Setter
private String challenge;
@Getter
private final long triggerPrice;
@Getter
@Setter
@ -111,12 +107,6 @@ public final class OpenOffer implements Tradable {
@Getter
@Setter
transient int numProcessingAttempts = 0;
@Getter
@Setter
private boolean deactivatedByTrigger;
@Getter
@Setter
private String groupId;
public OpenOffer(Offer offer) {
this(offer, 0, false);
@ -130,8 +120,6 @@ public final class OpenOffer implements Tradable {
this.offer = offer;
this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount;
this.challenge = offer.getChallenge();
this.groupId = UUID.randomUUID().toString();
state = State.PENDING;
}
@ -149,9 +137,6 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = openOffer.reserveTxHash;
this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey;
this.challenge = openOffer.challenge;
this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
this.groupId = openOffer.groupId;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -168,10 +153,7 @@ public final class OpenOffer implements Tradable {
long splitOutputTxFee,
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
@Nullable String reserveTxKey,
@Nullable String challenge,
boolean deactivatedByTrigger,
@Nullable String groupId) {
@Nullable String reserveTxKey) {
this.offer = offer;
this.state = state;
this.triggerPrice = triggerPrice;
@ -182,10 +164,6 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
this.challenge = challenge;
this.deactivatedByTrigger = deactivatedByTrigger;
if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19)
this.groupId = groupId;
// reset reserved state to available
if (this.state == State.RESERVED) setState(State.AVAILABLE);
@ -198,8 +176,7 @@ public final class OpenOffer implements Tradable {
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setSplitOutputTxFee(splitOutputTxFee)
.setReserveExactAmount(reserveExactAmount)
.setDeactivatedByTrigger(deactivatedByTrigger);
.setReserveExactAmount(reserveExactAmount);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
@ -207,8 +184,6 @@ public final class OpenOffer implements Tradable {
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@ -224,10 +199,7 @@ public final class OpenOffer implements Tradable {
proto.getSplitOutputTxFee(),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
proto.getDeactivatedByTrigger(),
ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()));
return openOffer;
}
@ -254,14 +226,6 @@ public final class OpenOffer implements Tradable {
public void setState(State state) {
this.state = state;
stateProperty.set(state);
if (state == State.AVAILABLE) {
deactivatedByTrigger = false;
}
}
public void deactivate(boolean deactivatedByTrigger) {
this.deactivatedByTrigger = deactivatedByTrigger;
setState(State.DEACTIVATED);
}
public ReadOnlyObjectProperty<State> stateProperty() {
@ -276,10 +240,6 @@ public final class OpenOffer implements Tradable {
return state == State.AVAILABLE;
}
public boolean isReserved() {
return state == State.RESERVED;
}
public boolean isDeactivated() {
return state == State.DEACTIVATED;
}
@ -297,7 +257,6 @@ public final class OpenOffer implements Tradable {
",\n reserveExactAmount=" + reserveExactAmount +
",\n scheduledAmount=" + scheduledAmount +
",\n splitOutputTxFee=" + splitOutputTxFee +
",\n groupId=" + groupId +
"\n}";
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -92,11 +92,12 @@ public class TriggerPriceService {
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
.filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
});
}
public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null || marketPrice == null) {
return false;
@ -124,12 +125,13 @@ public class TriggerPriceService {
}
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT;
if (wasTriggered(marketPrice, openOffer)) {
String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
TraditionalMoney.SMALLEST_UNIT_EXPONENT :
CryptoMoney.SMALLEST_UNIT_EXPONENT;
long triggerPrice = openOffer.getTriggerPrice();
if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
@ -137,26 +139,14 @@ public class TriggerPriceService {
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
);
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, () -> {
openOfferManager.deactivateOpenOffer(openOffer, () -> {
}, errorMessage -> {
});
} else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
// TODO: check if open offer's reserve tx is failed or double spend seen
}
}

View file

@ -88,8 +88,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
null, // reserve tx not sent from taker to maker
null,
null,
payoutAddress,
null); // challenge is required when offer taken
payoutAddress);
// save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest);

View file

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

View file

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

View file

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

View file

@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress);
// send request to random arbitrators until success
// send request to least used arbitrators until success
sendSignOfferRequests(request, () -> {
complete();
}, (errorMessage) -> {

View file

@ -19,13 +19,10 @@ package haveno.core.offer.placeoffer.tasks;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage;
import haveno.core.user.User;
import org.bitcoinj.core.Coin;
import java.math.BigInteger;
@ -44,7 +41,55 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
try {
runInterceptHook();
validateOffer(offer, model.getAccountAgeWitnessService(), model.getUser());
// Coins
checkBINotNullOrZero(offer.getAmount(), "Amount");
checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
//checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
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.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());
// We remove those checks to be more flexible with future changes.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
"createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
"MakerFee=" + offer.getMakerFee().toFriendlyString());*/
/*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
"sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
"sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = model.getAccountAgeWitnessService().getMyTradeLimit(model.getUser().getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection());
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null");
if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
"Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
checkArgument(offer.getDate().getTime() > 0,
"Date must not be 0. date=" + offer.getDate().toString());
checkNotNull(offer.getCurrencyCode(), "Currency is null");
checkNotNull(offer.getDirection(), "Direction is null");
checkNotNull(offer.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
checkNotNull(offer.getMinAmount(), "MinAmount is null");
checkNotNull(offer.getPrice(), "Price is null");
checkNotNull(offer.getVersionNr(), "VersionNr is null");
checkArgument(offer.getMaxTradePeriod() > 0,
"maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
// TODO check upper and lower bounds for fiat
// TODO check rest of new parameters
complete();
} catch (Exception e) {
@ -55,108 +100,42 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
}
}
public static void validateOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService, User user) {
// Coins
checkBINotNullOrZero(offer.getAmount(), "Amount");
checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
//checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
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());
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.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.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
"createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
"MakerFee=" + offer.getMakerFee().toFriendlyString());*/
/*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
"sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
"sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null");
if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
"Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
checkArgument(offer.getDate().getTime() > 0,
"Date must not be 0. date=" + offer.getDate().toString());
checkNotNull(offer.getCurrencyCode(), "Currency is null");
checkNotNull(offer.getDirection(), "Direction is null");
checkNotNull(offer.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
checkNotNull(offer.getMinAmount(), "MinAmount is null");
checkNotNull(offer.getPrice(), "Price is null");
checkNotNull(offer.getVersionNr(), "VersionNr is null");
checkArgument(offer.getMaxTradePeriod() > 0,
"maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
// TODO check upper and lower bounds for fiat
// TODO check rest of new parameters
}
private static void checkBINotNullOrZero(BigInteger value, String name) {
public static void checkBINotNullOrZero(BigInteger value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.compareTo(BigInteger.ZERO) > 0,
name + " must be positive. " + name + "=" + value);
}
private static void checkCoinNotNullOrZero(Coin value, String name) {
public static void checkCoinNotNullOrZero(Coin value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.isPositive(),
name + " must be positive. " + name + "=" + value.toFriendlyString());
}
private static String nonEmptyStringOf(String value) {
public static String nonEmptyStringOf(String value) {
checkNotNull(value);
checkArgument(value.length() > 0);
return value;
}
private static long nonNegativeLongOf(long value) {
public static long nonNegativeLongOf(long value) {
checkArgument(value >= 0);
return value;
}
private static Coin nonZeroCoinOf(Coin value) {
public static Coin nonZeroCoinOf(Coin value) {
checkNotNull(value);
checkArgument(!value.isZero());
return value;
}
private static Coin positiveCoinOf(Coin value) {
public static Coin positiveCoinOf(Coin value) {
checkNotNull(value);
checkArgument(value.isPositive());
return value;
}
private static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
public static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
checkArgument(tradeId.equals(tradeMessage.getOfferId()));
}
}

View file

@ -148,8 +148,7 @@ public class TakeOfferModel implements Model {
private long getMaxTradeLimit() {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(),
offer.getMirroredDirection(),
offer.hasBuyerAsTakerWithoutDeposit());
offer.getMirroredDirection());
}
@NotNull

View file

@ -31,34 +31,7 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public final class AliPayAccount extends PaymentAccount {
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 static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
public AliPayAccount() {
super(PaymentMethod.ALI_PAY);

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