diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1751c0d36..0f51adc8cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,18 @@ jobs: build: strategy: matrix: - os: [ubuntu-22.04, macos-13, windows-latest] + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-15, windows-2025] + include: + - os: ubuntu-24.04 + arch: x86_64 + - os: ubuntu-24.04-arm + arch: aarch64 + - os: macos-15-intel + arch: x86_64 + - os: macos-15 + arch: aarch64 + - os: windows-2025 + arch: x86_64 fail-fast: false runs-on: ${{ matrix.os }} steps: @@ -27,8 +38,25 @@ jobs: java-version: '21' distribution: 'adopt' cache: gradle - - name: Build with Gradle + - name: Create local share directory + # ubuntu-24.04 Github runners do not have `~/.local/share` directory by default. + # This causes issues when testing `FileTransferSend` + if: runner.os == 'Linux' + run: mkdir -p ~/.local/share + - name: Build with Gradle with tests + if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: ./gradlew build --stacktrace --scan + - name: Build with Gradle, skip Desktop tests + # JavaFX `21.x.x` ships with `x86_64` versions of + # `libprism_es2.so` and `libprism_s2.so` shared objects. + # This causes desktop tests to fail on `linux/aarch64` + if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }} + run: | + ./gradlew build --stacktrace --scan -x desktop:test + echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \ + intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \ + shared objects, causing tests to fail. \ + This should be revisited when JavaFX is next updated." - uses: actions/upload-artifact@v4 if: failure() with: @@ -38,115 +66,131 @@ jobs: uses: actions/upload-artifact@v4 with: include-hidden-files: true - name: cached-localnet + name: cached-localnet-${{ matrix.os }} path: .localnet overwrite: true - name: Install dependencies - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: Install WiX Toolset - if: ${{ matrix.os == 'windows-latest' }} + if: runner.os == 'Windows' run: | Invoke-WebRequest -Uri 'https://github.com/wixtoolset/wix3/releases/download/wix314rtm/wix314.exe' -OutFile wix314.exe .\wix314.exe /quiet /norestart shell: powershell - - name: Build Haveno Installer + - name: Build Haveno Installer with tests + if: ${{ !(runner.os == 'Linux' && matrix.arch == 'aarch64') }} + run: ./gradlew clean build --refresh-keys --refresh-dependencies + working-directory: . + - name: Build Haveno Installer, skip Desktop tests + # JavaFX `21.x.x` ships with `x86_64` versions of + # `libprism_es2.so` and `libprism_s2.so` shared objects. + # This causes desktop tests to fail on `linux/aarch64` + if: ${{ (runner.os == 'Linux' && matrix.arch == 'aarch64') }} run: | - ./gradlew clean build --refresh-keys --refresh-dependencies - ./gradlew packageInstallers + ./gradlew clean build --refresh-keys --refresh-dependencies -x desktop:test + echo "::warning title=Desktop Tests Skipped::Desktop tests (desktop:test) were \ + intentionally skipped for linux/aarch64 builds as JavaFX 21.x.x ships with x86_64 \ + shared objects, causing tests to fail. \ + This should be revisited when JavaFX is next updated." + working-directory: . + - name: Package Haveno Installer + run: ./gradlew packageInstallers working-directory: . # get version from jar - name: Set Version Unix - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} + if: runner.os != 'Windows' run: | export VERSION=$(ls desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 | grep -Eo 'desktop-[0-9]+\.[0-9]+\.[0-9]+' | sed 's/desktop-//') echo "VERSION=$VERSION" >> $GITHUB_ENV - name: Set Version Windows - if: ${{ matrix.os == 'windows-latest' }} + if: runner.os == 'Windows' run: | $VERSION = (Get-ChildItem -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256).Name -replace 'desktop-', '' -replace '-.*', '' "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append shell: powershell - name: Move Release Files for Linux - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' 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 + mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.rpm + mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-installer.deb + mv desktop/build/temp-*/binaries/*.flatpak ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.flatpak + mv desktop/build/temp-*/binaries/haveno_*.AppImage ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}.AppImage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-deb cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak - cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files for macOS - if: ${{ matrix.os == 'macos-13' }} + if: runner.os == 'MacOS' 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-macos-${{ matrix.arch }} + mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos-${{ matrix.arch }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-installer.dmg + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos-${{ matrix.arch }} + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-${{ matrix.arch }}-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files on Windows - if: ${{ matrix.os == 'windows-latest' }} + if: runner.os == 'Windows' run: | mkdir ${{ github.workspace }}/release-windows - Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe + Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-${{ matrix.arch }}-installer.exe Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 shell: powershell - # win + # Windows artifacts - uses: actions/upload-artifact@v4 name: "Windows artifacts" - if: ${{ matrix.os == 'windows-latest' }} + if: runner.os == 'Windows' with: - name: haveno-windows + name: haveno-windows-${{ matrix.arch }} path: ${{ github.workspace }}/release-windows - # macos + + # macOS artifacts - uses: actions/upload-artifact@v4 name: "macOS artifacts" - if: ${{ matrix.os == 'macos-13' }} + if: runner.os == 'MacOS' with: - name: haveno-macos - path: ${{ github.workspace }}/release-macos - # linux + name: haveno-macos-${{ matrix.arch }} + path: ${{ github.workspace }}/release-macos-${{ matrix.arch }} + + # Linux artifacts - uses: actions/upload-artifact@v4 name: "Linux - deb artifact" - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' with: - name: haveno-linux-deb + name: haveno-linux-${{ matrix.arch }}-deb path: ${{ github.workspace }}/release-linux-deb - uses: actions/upload-artifact@v4 name: "Linux - rpm artifact" - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' with: - name: haveno-linux-rpm + name: haveno-linux-${{ matrix.arch }}-rpm path: ${{ github.workspace }}/release-linux-rpm - uses: actions/upload-artifact@v4 name: "Linux - AppImage artifact" - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' with: - name: haveno-linux-appimage + name: haveno-linux-${{ matrix.arch }}-appimage path: ${{ github.workspace }}/release-linux-appimage - uses: actions/upload-artifact@v4 name: "Linux - flatpak artifact" - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: runner.os == 'Linux' with: - name: haveno-linux-flatpak + name: haveno-linux-${{ matrix.arch }}-flatpak path: ${{ github.workspace }}/release-linux-flatpak - name: Release @@ -154,14 +198,30 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: files: | + # Linux x86_64 ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 - ${{ 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 + + # Linux aarch64 + ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-aarch64-installer.deb + ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-aarch64-installer.rpm + ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-aarch64.AppImage + ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-aarch64.flatpak + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-aarch64-SNAPSHOT-all.jar.SHA-256 + + # macOS x86_64 + ${{ github.workspace }}/release-macos-x86_64/haveno-v${{ env.VERSION }}-macos-x86_64-installer.dmg + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-x86_64-SNAPSHOT-all.jar.SHA-256 + + # macOS aarch64 + ${{ github.workspace }}/release-macos-aarch64/haveno-v${{ env.VERSION }}-macos-aarch64-installer.dmg + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-aarch64-SNAPSHOT-all.jar.SHA-256 + + # Windows + ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-x86_64-installer.exe ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 # https://git-scm.com/docs/git-tag - git-tag Docu diff --git a/.github/workflows/codacy-code-reporter.yml b/.github/workflows/codacy-code-reporter.yml index be76ef35ef..9629ea492f 100644 --- a/.github/workflows/codacy-code-reporter.yml +++ b/.github/workflows/codacy-code-reporter.yml @@ -9,7 +9,7 @@ jobs: build: if: github.repository == 'haveno-dex/haveno' name: Publish coverage - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -19,6 +19,11 @@ jobs: java-version: '21' distribution: 'adopt' + - name: Create local share directory + # ubuntu-24.04 Github runners do not have `~/.local/share` directory by default. + # This causes issues when testing `FileTransferSend` + run: mkdir -p ~/.local/share + - name: Build with Gradle run: ./gradlew clean build -x checkstyleMain -x checkstyleTest -x shadowJar diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7e0fefe9e7..857a27c08e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 50ece9050c..4cd7cfa250 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -7,7 +7,7 @@ on: jobs: issueLabeled: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Bounty explanation uses: peter-evans/create-or-update-comment@v3 diff --git a/Makefile b/Makefile index 8f32c3ed41..6ff271ffed 100644 --- a/Makefile +++ b/Makefile @@ -70,11 +70,12 @@ 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 \ + --rpc-max-connections 1000 \ + --max-connections-per-ip 10 \ + --rpc-max-connections-per-private-ip 1000 \ monerod2-local: ./.localnet/monerod \ @@ -90,11 +91,12 @@ 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 \ + --rpc-max-connections 1000 \ + --max-connections-per-ip 10 \ + --rpc-max-connections-per-private-ip 1000 \ monerod3-local: ./.localnet/monerod \ @@ -110,11 +112,12 @@ 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 \ + --rpc-max-connections 1000 \ + --max-connections-per-ip 10 \ + --rpc-max-connections-per-private-ip 1000 \ #--proxy 127.0.0.1:49775 \ @@ -440,6 +443,9 @@ monerod: ./.localnet/monerod \ --bootstrap-daemon-address auto \ --rpc-access-control-origins http://localhost:8080 \ + --rpc-max-connections 1000 \ + --max-connections-per-ip 10 \ + --rpc-max-connections-per-private-ip 1000 \ seednode: ./haveno-seednode$(APP_EXT) \ @@ -485,6 +491,31 @@ arbitrator-desktop-mainnet: --xmrNode=http://127.0.0.1:18081 \ --useNativeXmrWallet=false \ +arbitrator2-daemon-mainnet: + ./haveno-daemon$(APP_EXT) \ + --baseCurrencyNetwork=XMR_MAINNET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=haveno-XMR_MAINNET_arbitrator2 \ + --apiPassword=apitest \ + --apiPort=1205 \ + --passwordRequired=false \ + --xmrNode=http://127.0.0.1:18081 \ + --useNativeXmrWallet=false \ + +arbitrator2-desktop-mainnet: + ./haveno-desktop$(APP_EXT) \ + --baseCurrencyNetwork=XMR_MAINNET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=haveno-XMR_MAINNET_arbitrator2 \ + --apiPassword=apitest \ + --apiPort=1205 \ + --xmrNode=http://127.0.0.1:18081 \ + --useNativeXmrWallet=false \ + haveno-daemon-mainnet: ./haveno-daemon$(APP_EXT) \ --baseCurrencyNetwork=XMR_MAINNET \ @@ -570,3 +601,19 @@ user3-desktop-mainnet: --apiPort=1204 \ --useNativeXmrWallet=false \ --ignoreLocalXmrNode=false \ + +buyer-wallet-mainnet: + ./.localnet/monero-wallet-rpc \ + --daemon-address http://localhost:18081 \ + --rpc-bind-port 18084 \ + --rpc-login rpc_user:abc123 \ + --rpc-access-control-origins http://localhost:8080 \ + --wallet-dir ./.localnet \ + +seller-wallet-mainnet: + ./.localnet/monero-wallet-rpc \ + --daemon-address http://localhost:18081 \ + --rpc-bind-port 18085 \ + --rpc-login rpc_user:abc123 \ + --rpc-access-control-origins http://localhost:8080 \ + --wallet-dir ./.localnet \ diff --git a/README.md b/README.md index 4e90b06ce3..ab232aafa7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Haveno logo - ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master) + [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)](https://github.com/haveno-dex/haveno/actions) [![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) [![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex) [![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md) @@ -67,19 +67,17 @@ See the [developer guide](docs/developer-guide.md) to get started developing for See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. -If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties. +If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties. ## Bounties To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md). -## Support and sponsorships +## Support -To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project: +To bring Haveno to life, we need resources. If you have the possibility, please consider donating to the project: -

+

Donate Monero
- 42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F + 47fo8N5m2VVW4uojadGQVJ34LFR9yXwDrZDRugjvVSjcTWV2WFSoc1XfNpHmxwmVtfNY9wMBch6259G6BXXFmhU49YG1zfB

- -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. diff --git a/apitest/src/test/java/haveno/apitest/method/MethodTest.java b/apitest/src/test/java/haveno/apitest/method/MethodTest.java index 01c7a3bfd3..0007cc6c3a 100644 --- a/apitest/src/test/java/haveno/apitest/method/MethodTest.java +++ b/apitest/src/test/java/haveno/apitest/method/MethodTest.java @@ -43,7 +43,7 @@ import java.util.stream.Collectors; import static haveno.apitest.config.ApiTestConfig.BTC; import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig; import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL; -import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; @@ -158,7 +158,7 @@ public class MethodTest extends ApiTestCase { } public static final Supplier defaultSecurityDepositPct = () -> { - var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent()); + var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositPct()); if (defaultPct.precision() != 2) throw new IllegalStateException(format( "Unexpected decimal precision, expected 2 but actual is %d%n." diff --git a/assets/src/main/java/haveno/asset/CardanoAddressValidator.java b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java new file mode 100644 index 0000000000..0b3546e4fb --- /dev/null +++ b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java @@ -0,0 +1,106 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset; + +/** + * Validates a Shelley-era mainnet Cardano address. + */ +public class CardanoAddressValidator extends RegexAddressValidator { + + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + private static final int BECH32_CONST = 1; + private static final int BECH32M_CONST = 0x2bc830a3; + private static final int MAX_LEN = 104; // bech32 / bech32m max for Cardano + + public CardanoAddressValidator() { + super("^addr1[0-9a-z]{20,98}$"); + } + + public CardanoAddressValidator(String errorMessageI18nKey) { + super("^addr1[0-9a-z]{20,98}$", errorMessageI18nKey); + } + + @Override + public AddressValidationResult validate(String address) { + if (!isValidShelleyMainnet(address)) { + return AddressValidationResult.invalidStructure(); + } + return super.validate(address); + } + + /** + * Checks if the given address is a valid Shelley-era mainnet Cardano address. + * + * This code is AI-generated and has been tested with a variety of addresses. + * + * @param addr the address to validate + * @return true if the address is valid, false otherwise + */ + private static boolean isValidShelleyMainnet(String addr) { + if (addr == null) return false; + String lower = addr.toLowerCase(); + + // must start addr1 and not be absurdly long + if (!lower.startsWith("addr1") || lower.length() > MAX_LEN) return false; + + int sep = lower.lastIndexOf('1'); + if (sep < 1) return false; // no separator or empty HRP + String hrp = lower.substring(0, sep); + if (!"addr".equals(hrp)) return false; // mainnet only + + String dataPart = lower.substring(sep + 1); + if (dataPart.length() < 6) return false; // checksum is 6 chars minimum + + int[] data = new int[dataPart.length()]; + for (int i = 0; i < dataPart.length(); i++) { + int v = CHARSET.indexOf(dataPart.charAt(i)); + if (v == -1) return false; + data[i] = v; + } + + int[] hrpExp = hrpExpand(hrp); + int[] combined = new int[hrpExp.length + data.length]; + System.arraycopy(hrpExp, 0, combined, 0, hrpExp.length); + System.arraycopy(data, 0, combined, hrpExp.length, data.length); + + int chk = polymod(combined); + return chk == BECH32_CONST || chk == BECH32M_CONST; // accept either legacy Bech32 (1) or Bech32m (0x2bc830a3) + } + + private static int[] hrpExpand(String hrp) { + int[] ret = new int[hrp.length() * 2 + 1]; + int idx = 0; + for (char c : hrp.toCharArray()) ret[idx++] = c >> 5; + ret[idx++] = 0; + for (char c : hrp.toCharArray()) ret[idx++] = c & 31; + return ret; + } + + private static int polymod(int[] values) { + int chk = 1; + int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}; + for (int v : values) { + int b = chk >>> 25; + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (int i = 0; i < 5; i++) { + if (((b >>> i) & 1) != 0) chk ^= GEN[i]; + } + } + return chk; + } +} diff --git a/assets/src/main/java/haveno/asset/RippleAddressValidator.java b/assets/src/main/java/haveno/asset/RippleAddressValidator.java new file mode 100644 index 0000000000..7325818143 --- /dev/null +++ b/assets/src/main/java/haveno/asset/RippleAddressValidator.java @@ -0,0 +1,32 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset; + +/** + * Validates a Ripple address using a regular expression. + */ +public class RippleAddressValidator extends RegexAddressValidator { + + public RippleAddressValidator() { + super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$"); + } + + public RippleAddressValidator(String errorMessageI18nKey) { + super("^r[1-9A-HJ-NP-Za-km-z]{25,34}$", errorMessageI18nKey); + } +} diff --git a/assets/src/main/java/haveno/asset/SolanaAddressValidator.java b/assets/src/main/java/haveno/asset/SolanaAddressValidator.java new file mode 100644 index 0000000000..92ff6468de --- /dev/null +++ b/assets/src/main/java/haveno/asset/SolanaAddressValidator.java @@ -0,0 +1,94 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset; + +import java.math.BigInteger; + +/** + * Validates a Solana address. + */ +public class SolanaAddressValidator implements AddressValidator { + + private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public SolanaAddressValidator() { + } + + @Override + public AddressValidationResult validate(String address) { + if (!isValidSolanaAddress(address)) { + return AddressValidationResult.invalidStructure(); + } + return AddressValidationResult.validAddress(); + } + + /** + * Checks if the given address is a valid Solana address. + * + * This code is AI-generated and has been tested with a variety of addresses. + * + * @param addr the address to validate + * @return true if the address is valid, false otherwise + */ + private static boolean isValidSolanaAddress(String address) { + if (address == null) return false; + if (address.length() < 32 || address.length() > 44) return false; // typical Solana length range + + // Check all chars are base58 valid + for (char c : address.toCharArray()) { + if (BASE58_ALPHABET.indexOf(c) == -1) return false; + } + + // Decode from base58 and ensure exactly 32 bytes + byte[] decoded = decodeBase58(address); + return decoded != null && decoded.length == 32; + } + + private static byte[] decodeBase58(String input) { + BigInteger num = BigInteger.ZERO; + BigInteger base = BigInteger.valueOf(58); + + for (char c : input.toCharArray()) { + int digit = BASE58_ALPHABET.indexOf(c); + if (digit < 0) return null; // invalid char + num = num.multiply(base).add(BigInteger.valueOf(digit)); + } + + // Convert BigInteger to byte array + byte[] bytes = num.toByteArray(); + + // Remove sign byte if present + if (bytes.length > 1 && bytes[0] == 0) { + byte[] tmp = new byte[bytes.length - 1]; + System.arraycopy(bytes, 1, tmp, 0, tmp.length); + bytes = tmp; + } + + // Count leading '1's and add leading zero bytes + int leadingZeros = 0; + for (char c : input.toCharArray()) { + if (c == '1') leadingZeros++; + else break; + } + + byte[] result = new byte[leadingZeros + bytes.length]; + System.arraycopy(bytes, 0, result, leadingZeros, bytes.length); + + return result; + } +} diff --git a/assets/src/main/java/haveno/asset/TronAddressValidator.java b/assets/src/main/java/haveno/asset/TronAddressValidator.java new file mode 100644 index 0000000000..6125975621 --- /dev/null +++ b/assets/src/main/java/haveno/asset/TronAddressValidator.java @@ -0,0 +1,104 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.util.Arrays; + +/** + * Validates a Tron address. + */ +public class TronAddressValidator implements AddressValidator { + + private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final byte MAINNET_PREFIX = 0x41; + + public TronAddressValidator() { + } + + @Override + public AddressValidationResult validate(String address) { + if (!isValidTronAddress(address)) { + return AddressValidationResult.invalidStructure(); + } + return AddressValidationResult.validAddress(); + } + + /** + * Checks if the given address is a valid Solana address. + * + * This code is AI-generated and has been tested with a variety of addresses. + * + * @param addr the address to validate + * @return true if the address is valid, false otherwise + */ + private static boolean isValidTronAddress(String address) { + if (address == null || address.length() != 34) return false; + + byte[] decoded = decodeBase58(address); + if (decoded == null || decoded.length != 25) return false; // 21 bytes data + 4 bytes checksum + + // Check checksum + byte[] data = Arrays.copyOfRange(decoded, 0, 21); + byte[] checksum = Arrays.copyOfRange(decoded, 21, 25); + byte[] calculatedChecksum = Arrays.copyOfRange(doubleSHA256(data), 0, 4); + + if (!Arrays.equals(checksum, calculatedChecksum)) return false; + + // Check mainnet prefix + return data[0] == MAINNET_PREFIX; + } + + private static byte[] decodeBase58(String input) { + BigInteger num = BigInteger.ZERO; + BigInteger base = BigInteger.valueOf(58); + + for (char c : input.toCharArray()) { + int digit = BASE58_ALPHABET.indexOf(c); + if (digit < 0) return null; + num = num.multiply(base).add(BigInteger.valueOf(digit)); + } + + // Convert BigInteger to byte array + byte[] bytes = num.toByteArray(); + if (bytes.length > 1 && bytes[0] == 0) { + bytes = Arrays.copyOfRange(bytes, 1, bytes.length); + } + + // Add leading zero bytes for '1's + int leadingZeros = 0; + for (char c : input.toCharArray()) { + if (c == '1') leadingZeros++; + else break; + } + + byte[] result = new byte[leadingZeros + bytes.length]; + System.arraycopy(bytes, 0, result, leadingZeros, bytes.length); + return result; + } + + private static byte[] doubleSHA256(byte[] data) { + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + return sha256.digest(sha256.digest(data)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Cardano.java b/assets/src/main/java/haveno/asset/coins/Cardano.java new file mode 100644 index 0000000000..16f2563930 --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Cardano.java @@ -0,0 +1,28 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.CardanoAddressValidator; +import haveno.asset.Coin; + +public class Cardano extends Coin { + + public Cardano() { + super("Cardano", "ADA", new CardanoAddressValidator()); + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Dogecoin.java b/assets/src/main/java/haveno/asset/coins/Dogecoin.java new file mode 100644 index 0000000000..f0743861ae --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Dogecoin.java @@ -0,0 +1,36 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.Base58AddressValidator; +import haveno.asset.Coin; +import haveno.asset.NetworkParametersAdapter; + +public class Dogecoin extends Coin { + + public Dogecoin() { + super("Dogecoin", "DOGE", new Base58AddressValidator(new DogecoinMainNetParams()), Network.MAINNET); + } + + public static class DogecoinMainNetParams extends NetworkParametersAdapter { + public DogecoinMainNetParams() { + this.addressHeader = 30; + this.p2shHeader = 22; + } + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Ripple.java b/assets/src/main/java/haveno/asset/coins/Ripple.java new file mode 100644 index 0000000000..04ce84475d --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Ripple.java @@ -0,0 +1,28 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.Coin; +import haveno.asset.RippleAddressValidator; + +public class Ripple extends Coin { + + public Ripple() { + super("Ripple", "XRP", new RippleAddressValidator()); + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Solana.java b/assets/src/main/java/haveno/asset/coins/Solana.java new file mode 100644 index 0000000000..e2a035601d --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Solana.java @@ -0,0 +1,28 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.Coin; +import haveno.asset.SolanaAddressValidator; + +public class Solana extends Coin { + + public Solana() { + super("Solana", "SOL", new SolanaAddressValidator()); + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Tron.java b/assets/src/main/java/haveno/asset/coins/Tron.java new file mode 100644 index 0000000000..d358834eeb --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Tron.java @@ -0,0 +1,28 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.Coin; +import haveno.asset.TronAddressValidator; + +public class Tron extends Coin { + + public Tron() { + super("Tron", "TRX", new TronAddressValidator()); + } +} diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java index 1afb7ff1f2..ffbcac2cd3 100644 --- a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java @@ -6,6 +6,6 @@ public class TetherUSDERC20 extends Erc20Token { public TetherUSDERC20() { // If you add a new USDT variant or want to change this ticker symbol you should also look here: // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (ERC20)", "USDT-ERC20"); + super("Tether USD", "USDT-ERC20"); } } diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java index c5669d126a..c12bb37442 100644 --- a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java @@ -6,6 +6,6 @@ public class TetherUSDTRC20 extends Trc20Token { public TetherUSDTRC20() { // If you add a new USDT variant or want to change this ticker symbol you should also look here: // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (TRC20)", "USDT-TRC20"); + super("Tether USD", "USDT-TRC20"); } } diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java index a65c021df9..cb371bd221 100644 --- a/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java +++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java @@ -22,6 +22,6 @@ import haveno.asset.Erc20Token; public class USDCoinERC20 extends Erc20Token { public USDCoinERC20() { - super("USD Coin (ERC20)", "USDC-ERC20"); + super("USD Coin", "USDC-ERC20"); } } diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset index 80b9cd036d..018fd76211 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -4,9 +4,14 @@ # See https://haveno.exchange/list-asset for complete instructions. haveno.asset.coins.Bitcoin$Mainnet haveno.asset.coins.BitcoinCash +haveno.asset.coins.Cardano +haveno.asset.coins.Dogecoin haveno.asset.coins.Ether haveno.asset.coins.Litecoin haveno.asset.coins.Monero +haveno.asset.coins.Ripple +haveno.asset.coins.Solana +haveno.asset.coins.Tron haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDTRC20 haveno.asset.tokens.USDCoinERC20 diff --git a/assets/src/main/resources/i18n/displayStrings-assets.properties b/assets/src/main/resources/i18n/displayStrings-assets.properties index ae23634d1c..5d67b53eab 100644 --- a/assets/src/main/resources/i18n/displayStrings-assets.properties +++ b/assets/src/main/resources/i18n/displayStrings-assets.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break diff --git a/assets/src/test/java/haveno/asset/coins/CardanoTest.java b/assets/src/test/java/haveno/asset/coins/CardanoTest.java new file mode 100644 index 0000000000..bae8141aba --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/CardanoTest.java @@ -0,0 +1,42 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; +import org.junit.jupiter.api.Test; + +public class CardanoTest extends AbstractAssetTest { + + public CardanoTest() { + super(new Cardano()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); + assertValidAddress("addr1q8gg2r3vf9zggn48g7m8vx62rwf6warcs4k7ej8mdzmqmesj30jz7psduyk6n4n2qrud2xlv9fgj53n6ds3t8cs4fvzs05yzmz"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("addr1Q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); + assertInvalidAddress("addr2q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); + assertInvalidAddress("addr2vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); + assertInvalidAddress("Ae2tdPwUPEYxkYw5GrFyqb4Z9TzXo8f1WnWpPZP1sXrEn1pz2VU3CkJ8aTQ"); + } +} diff --git a/assets/src/test/java/haveno/asset/coins/DogecoinTest.java b/assets/src/test/java/haveno/asset/coins/DogecoinTest.java new file mode 100644 index 0000000000..5e0e450a39 --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/DogecoinTest.java @@ -0,0 +1,43 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; + +import org.junit.jupiter.api.Test; + +public class DogecoinTest extends AbstractAssetTest { + + public DogecoinTest() { + super(new Dogecoin()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("DEa7damK8MsbdCJztidBasZKVsDLJifWfE"); + assertValidAddress("DNkkfdUvkCDiywYE98MTVp9nQJTgeZAiFr"); + assertValidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("1DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg"); + assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxgs"); + assertInvalidAddress("DDWUYQ3GfMDj8hkx8cbnAMYkTzzAunAQxg#"); + } +} diff --git a/assets/src/test/java/haveno/asset/coins/RippleTest.java b/assets/src/test/java/haveno/asset/coins/RippleTest.java new file mode 100644 index 0000000000..d984d78174 --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/RippleTest.java @@ -0,0 +1,44 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; +import org.junit.jupiter.api.Test; + +public class RippleTest extends AbstractAssetTest { + + public RippleTest() { + super(new Ripple()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("r9CxAMAoZAgyVGP8CY9F1arzf9bJg3Y7U8"); + assertValidAddress("rsXMbDtCAmzSWajWiii7ffWygAjYVNDxY7"); + assertValidAddress("rE3nYkQy121JEVb37JKX8LSH6wUBnNvNo2"); + assertValidAddress("rMzucuWFUEE6aM9DC992BqqMgZNPrv4kvi"); + assertValidAddress("rJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("RJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); + assertInvalidAddress("zJUmAFPWE36cpdbN4DUEAFBLtG2xkEavY8"); + assertInvalidAddress("1LgfapHEPhZbRF9pMd5WPT35hFXcZS1USrW"); + } +} diff --git a/assets/src/test/java/haveno/asset/coins/SolanaTest.java b/assets/src/test/java/haveno/asset/coins/SolanaTest.java new file mode 100644 index 0000000000..a6ef27f770 --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/SolanaTest.java @@ -0,0 +1,46 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; +import org.junit.jupiter.api.Test; + +public class SolanaTest extends AbstractAssetTest { + + public SolanaTest() { + super(new Solana()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33k"); + assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz7"); + assertValidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpsk"); + assertValidAddress("7zVhJcA5s8zfg3UoDUuG4zmnqaVmLqj6L6F6L8WPLnYw"); + assertValidAddress("AVHUu155WoNexeNCGce8mrb8hvg8pBgvCJh4vtd3Q1RV"); + assertValidAddress("8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NqAcoH7okh4wz"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("4Nd1mYZbtJbHkj9QwxAXWah8X9M8vZ9H1fsn6uhPW33O"); + assertInvalidAddress("H3C5pGrMmD8FrGd9VRtNVbY3tWusJX3A1u33f9bdBpskAAA"); + assertInvalidAddress("1"); + assertInvalidAddress("abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"); + } +} diff --git a/assets/src/test/java/haveno/asset/coins/TronTest.java b/assets/src/test/java/haveno/asset/coins/TronTest.java new file mode 100644 index 0000000000..dd70d2edea --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/TronTest.java @@ -0,0 +1,45 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; +import org.junit.jupiter.api.Test; + +public class TronTest extends AbstractAssetTest { + + public TronTest() { + super(new Tron()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz"); + assertValidAddress("THdUXD3mZqT5aMnPQMtBSJX9ANGjaeUwQK"); + assertValidAddress("THUE6WTLaEGytFyuGJQUcKc3r245UKypoi"); + assertValidAddress("TH7vVF9RTMXM9x7ZnPnbNcEph734hpu8cf"); + assertValidAddress("TJNtFduS4oebw3jgGKCYmgSpTdyPieb6Ha"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); + assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9X"); + assertInvalidAddress("1JRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); + assertInvalidAddress("TGzz8gjYiYRqpfmDwnLxfgPuLVNmpCswVo"); + } +} diff --git a/build.gradle b/build.gradle index 20ca924031..a684d51d49 100644 --- a/build.gradle +++ b/build.gradle @@ -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.38' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' @@ -79,7 +79,9 @@ configure(subprojects) { slf4jVersion = '1.7.30' sparkVersion = '2.5.2' - os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os + def osName = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os + def osArch = System.getProperty("os.arch").toLowerCase() + os = (osName == 'mac' && (osArch.contains('aarch64') || osArch.contains('arm'))) ? 'mac-aarch64' : osName } repositories { @@ -457,14 +459,14 @@ configure(project(':core')) { doLast { // get monero binaries download url Map moneroBinaries = [ - 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz', - 'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208', - 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz', - 'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c', - 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz', - 'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8', - 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip', - 'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d' + 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-x86_64.tar.gz', + 'linux-x86_64-sha256' : '713d64ff6423add0d065d9dfbf8a120dfbf3995d4b2093f8235b4da263d8a89c', + 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-linux-aarch64.tar.gz', + 'linux-aarch64-sha256' : '332dcc6a5d7eec754c010a1f893f81656be1331b847b06e9be69293b456f67cc', + 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-mac.tar.gz', + 'mac-sha256' : '1c5bcd23373132528634352e604c1d732a73c634f3c77314fae503c6d23e10b0', + 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release7/monero-bins-haveno-windows.zip', + 'windows-sha256' : '3d57b980e0208a950fd795f442d9e087b5298a914b0bd96fec431188b5ab0dad' ] String osKey @@ -610,7 +612,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.19-SNAPSHOT' + version = '1.2.1-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 3dc04889b8..25dedd2543 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.19"; + public static final String VERSION = "1.2.1"; /** * Holds a list of the tagged resource files for optimizing the getData requests. @@ -107,12 +107,11 @@ public class Version { // The version no. of the current protocol. The offer holds that version. // A taker will check the version of the offers to see if his version is compatible. - // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. - // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening // the Haveno app. // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 - public static final int TRADE_PROTOCOL_VERSION = 2; + // Version = 1.2.0 -> TRADE_PROTOCOL_VERSION = 3 + public static final int TRADE_PROTOCOL_VERSION = 3; private static String p2pMessageVersion; public static String getP2PMessageVersion() { diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java index b162e211b4..03a61cbd40 100644 --- a/common/src/main/java/haveno/common/config/Config.java +++ b/common/src/main/java/haveno/common/config/Config.java @@ -119,6 +119,7 @@ public class Config { public static final String PASSWORD_REQUIRED = "passwordRequired"; public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; + public static final String DISABLE_RATE_LIMITS = "disableRateLimits"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -208,6 +209,7 @@ public class Config { public final boolean passwordRequired; public final boolean updateXmrBinaries; public final String xmrBlockchainPath; + public final boolean disableRateLimits; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -639,6 +641,13 @@ public class Config { .ofType(String.class) .defaultsTo(""); + ArgumentAcceptingOptionSpec disableRateLimits = + parser.accepts(DISABLE_RATE_LIMITS, + "Disables all API rate limits") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -753,6 +762,7 @@ public class Config { this.passwordRequired = options.valueOf(passwordRequiredOpt); this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); + this.disableRateLimits = options.valueOf(disableRateLimits); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/common/src/main/java/haveno/common/persistence/PersistenceManager.java b/common/src/main/java/haveno/common/persistence/PersistenceManager.java index b9d38bd82f..88269082fb 100644 --- a/common/src/main/java/haveno/common/persistence/PersistenceManager.java +++ b/common/src/main/java/haveno/common/persistence/PersistenceManager.java @@ -433,12 +433,14 @@ public class PersistenceManager { private void maybeStartTimerForPersistence() { // We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays // can be rather long. - if (timer == null) { - timer = UserThread.runAfter(() -> { - persistNow(null); - UserThread.execute(() -> timer = null); - }, source.delay, TimeUnit.MILLISECONDS); - } + UserThread.execute(() -> { + if (timer == null) { + timer = UserThread.runAfter(() -> { + persistNow(null); + UserThread.execute(() -> timer = null); + }, source.delay, TimeUnit.MILLISECONDS); + } + }); } public void forcePersistNow() { diff --git a/common/src/main/java/haveno/common/util/MathUtils.java b/common/src/main/java/haveno/common/util/MathUtils.java index 25c91ed254..a89e4bc01e 100644 --- a/common/src/main/java/haveno/common/util/MathUtils.java +++ b/common/src/main/java/haveno/common/util/MathUtils.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayDeque; import java.util.Deque; @@ -85,6 +86,11 @@ public class MathUtils { return ((double) value) * factor; } + public static BigInteger scaleUpByPowerOf10(BigInteger value, int exponent) { + BigInteger factor = BigInteger.TEN.pow(exponent); + return value.multiply(factor); + } + public static double scaleDownByPowerOf10(double value, int exponent) { double factor = Math.pow(10, exponent); return value / factor; @@ -95,6 +101,11 @@ public class MathUtils { return ((double) value) / factor; } + public static BigInteger scaleDownByPowerOf10(BigInteger value, int exponent) { + BigInteger factor = BigInteger.TEN.pow(exponent); + return value.divide(factor); + } + public static double exactMultiply(double value1, double value2) { return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); } diff --git a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java index b4ba7b58a8..f86a0c2bb2 100644 --- a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java @@ -335,12 +335,13 @@ public class SignedWitnessService { String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash()); String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8); ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey()); - if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) { + String pubKeyHex = Utilities.encodeToHex(key.getPubKey()); + if (arbitratorManager.isPublicKeyInList(pubKeyHex)) { key.verifyMessage(message, signatureBase64); verifySignatureWithECKeyResultCache.put(hash, true); return true; } else { - log.warn("Provided EC key is not in list of valid arbitrators."); + log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex); verifySignatureWithECKeyResultCache.put(hash, false); return false; } diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java index 978b6dd715..eb36f0aa0f 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java @@ -654,7 +654,7 @@ public class AccountAgeWitnessService { Date peersCurrentDate, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer); - final String currencyCode = offer.getCurrencyCode(); + final String currencyCode = offer.getCounterCurrencyCode(); final BigInteger defaultMaxTradeLimit = offer.getPaymentMethod().getMaxTradeLimit(currencyCode); BigInteger peersCurrentTradeLimit = defaultMaxTradeLimit; if (!hasTradeLimitException(peersWitness)) { @@ -673,7 +673,7 @@ public class AccountAgeWitnessService { "\nPeers trade limit=" + peersCurrentTradeLimit + "\nOffer ID=" + offer.getShortId() + "\nPaymentMethod=" + offer.getPaymentMethod().getId() + - "\nCurrencyCode=" + offer.getCurrencyCode(); + "\nCurrencyCode=" + offer.getCounterCurrencyCode(); log.warn(msg); errorMessageHandler.handleErrorMessage(msg); } diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java index ed5645b910..cdce0e0079 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessUtils.java @@ -140,7 +140,7 @@ public class AccountAgeWitnessUtils { boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && !accountAgeWitnessService.peerHasSignedWitness(trade) && accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()); - log.info("AccountSigning debug log: " + + log.debug("AccountSigning debug log: " + "\ntradeId: {}" + "\nis buyer: {}" + "\nbuyer account age witness info: {}" + diff --git a/core/src/main/java/haveno/core/alert/AlertManager.java b/core/src/main/java/haveno/core/alert/AlertManager.java index a54f45c489..8734509ab2 100644 --- a/core/src/main/java/haveno/core/alert/AlertManager.java +++ b/core/src/main/java/haveno/core/alert/AlertManager.java @@ -105,9 +105,9 @@ public class AlertManager { "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); case XMR_STAGENET: return List.of( - "036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f", - "026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854", - "025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36"); + "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", + "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", + "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of(); default: diff --git a/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java b/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java index fd6abac552..9abc74f564 100644 --- a/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java +++ b/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java @@ -104,9 +104,9 @@ public class PrivateNotificationManager implements MessageListener { "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492"); case XMR_STAGENET: return List.of( - "02ba7c5de295adfe57b60029f3637a2c6b1d0e969a8aaefb9e0ddc3a7963f26925", - "026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854", - "025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36"); + "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", + "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", + "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); case XMR_MAINNET: return List.of(); default: diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index e8e83978eb..5162bfdb33 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -299,8 +299,12 @@ public class CoreApi { return walletsService.createXmrTx(destinations); } - public String relayXmrTx(String metadata) { - return walletsService.relayXmrTx(metadata); + public List createXmrSweepTxs(String address) { + return walletsService.createXmrSweepTxs(address); + } + + public List relayXmrTxs(List metadatas) { + return walletsService.relayXmrTxs(metadatas); } public long getAddressBalance(String addressString) { diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index f4bb4c803d..ef6d56472b 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -241,12 +241,24 @@ public class CoreDisputesService { } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed + log.warn("Payout amount for buyer is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}", + HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()), + HavenoUtils.formatXmr(trade.getWallet().getBalance())); + disputeResult.setBuyerPayoutAmountBeforeCost(trade.getWallet().getBalance()); + } } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed + log.warn("Payout amount for seller is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}", + HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()), + HavenoUtils.formatXmr(trade.getWallet().getBalance())); + disputeResult.setSellerPayoutAmountBeforeCost(trade.getWallet().getBalance()); + } } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) { if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance"); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 3ee7e047f1..a3b044f3b1 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -149,7 +149,7 @@ public class CoreOffersService { List getMyOffers(String direction, String currencyCode) { return getMyOffers().stream() .filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode)) - .sorted(openOfferPriceComparator(direction, CurrencyUtil.isTraditionalCurrency(currencyCode))) + .sorted(openOfferPriceComparator(direction)) .collect(Collectors.toList()); } @@ -336,7 +336,7 @@ public class CoreOffersService { String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { - long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode()); + long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCounterCurrencyCode()); openOfferManager.placeOffer(offer, useSavingsWallet, triggerPriceAsLong, @@ -353,8 +353,7 @@ public class CoreOffersService { if ("".equals(direction)) direction = null; if ("".equals(currencyCode)) currencyCode = null; var offerOfWantedDirection = direction == null || offer.getDirection().name().equalsIgnoreCase(direction); - var counterAssetCode = CurrencyUtil.isCryptoCurrency(currencyCode) ? offer.getOfferPayload().getBaseCurrencyCode() : offer.getOfferPayload().getCounterCurrencyCode(); - var offerInWantedCurrency = currencyCode == null || counterAssetCode.equalsIgnoreCase(currencyCode); + var offerInWantedCurrency = currencyCode == null || offer.getCounterCurrencyCode().equalsIgnoreCase(currencyCode); return offerOfWantedDirection && offerInWantedCurrency; } @@ -366,17 +365,12 @@ public class CoreOffersService { : priceComparator.get(); } - private Comparator openOfferPriceComparator(String direction, boolean isTraditional) { + private Comparator openOfferPriceComparator(String direction) { // A buyer probably wants to see sell orders in price ascending order. // A seller probably wants to see buy orders in price descending order. - if (isTraditional) - return direction.equalsIgnoreCase(OfferDirection.BUY.name()) - ? openOfferPriceComparator.get().reversed() - : openOfferPriceComparator.get(); - else - return direction.equalsIgnoreCase(OfferDirection.SELL.name()) - ? openOfferPriceComparator.get().reversed() - : openOfferPriceComparator.get(); + return direction.equalsIgnoreCase(OfferDirection.BUY.name()) + ? openOfferPriceComparator.get().reversed() + : openOfferPriceComparator.get(); } private long priceStringToLong(String priceAsString, String currencyCode) { diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java index b506a00ac1..b6afd098b7 100644 --- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java @@ -36,6 +36,8 @@ import haveno.core.payment.InstantCryptoCurrencyAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.validation.InteracETransferValidator; +import haveno.core.trade.HavenoUtils; import haveno.core.user.User; import java.io.File; import static java.lang.String.format; @@ -48,19 +50,24 @@ import lombok.extern.slf4j.Slf4j; @Singleton @Slf4j -class CorePaymentAccountsService { +public class CorePaymentAccountsService { private final CoreAccountService accountService; private final AccountAgeWitnessService accountAgeWitnessService; private final User user; + public final InteracETransferValidator interacETransferValidator; @Inject public CorePaymentAccountsService(CoreAccountService accountService, AccountAgeWitnessService accountAgeWitnessService, - User user) { + User user, + InteracETransferValidator interacETransferValidator) { this.accountService = accountService; this.accountAgeWitnessService = accountAgeWitnessService; this.user = user; + this.interacETransferValidator = interacETransferValidator; + + HavenoUtils.corePaymentAccountService = this; } PaymentAccount createPaymentAccount(PaymentAccountForm form) { diff --git a/core/src/main/java/haveno/core/api/CorePriceService.java b/core/src/main/java/haveno/core/api/CorePriceService.java index ddd194ebab..d4ed3257cf 100644 --- a/core/src/main/java/haveno/core/api/CorePriceService.java +++ b/core/src/main/java/haveno/core/api/CorePriceService.java @@ -74,9 +74,11 @@ class CorePriceService { public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); if (marketPrice == null) { - throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client + throw new IllegalArgumentException("Currency not found: " + currencyCode); // TODO: do not use IllegalArgumentException as message sent to client, return undefined? + } else if (!marketPrice.isExternallyProvidedPrice()) { + throw new IllegalArgumentException("Price is not available externally: " + currencyCode); // TODO: return more complex Price type including price double and isExternal boolean } - return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); + return marketPrice.getPrice(); } /** @@ -85,8 +87,7 @@ class CorePriceService { public List getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException { return priceFeedService.requestAllPrices().values().stream() .map(marketPrice -> { - double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); - return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice); + return new MarketPriceInfo(marketPrice.getCurrencyCode(), marketPrice.getPrice()); }) .collect(Collectors.toList()); } @@ -100,12 +101,13 @@ class CorePriceService { // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. Comparator offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); + // TODO: remove this!!! // Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is // considered as buying/selling crypto. Because of this, when viewing a xmr-crypto pair, // the buy column is actually the sell column and vice versa. To maintain the expected // ordering, we have to reverse the price comparator. - boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode); - if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); + //boolean isCrypto = CurrencyUtil.isCryptoCurrency(currencyCode); + //if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); // Offer amounts are used for the secondary sort. They are sorted from high to low. Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); @@ -128,11 +130,11 @@ class CorePriceService { double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); accumulatedAmount += amount; double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); - buyTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); + buyTM.put(priceAsDouble, accumulatedAmount); } }; - // Create buyer hashmap {key:price, value:count}, uses TreeMap to sort by key (asc) + // Create seller hashmap {key:price, value:count}, uses TreeMap to sort by key (asc) accumulatedAmount = 0; LinkedHashMap sellTM = new LinkedHashMap(); for(Offer offer: sellOffers){ @@ -141,7 +143,7 @@ class CorePriceService { double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); accumulatedAmount += amount; double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); - sellTM.put(mapPriceFeedServicePrice(priceAsDouble, currencyCode), accumulatedAmount); + sellTM.put(priceAsDouble, accumulatedAmount); } }; @@ -155,20 +157,5 @@ class CorePriceService { return new MarketDepthInfo(currencyCode, buyPrices, buyDepth, sellPrices, sellDepth); } - - /** - * PriceProvider returns different values for crypto and traditional, - * e.g. 1 XMR = X USD - * but 1 DOGE = X XMR - * Here we convert all to: - * 1 XMR = X (FIAT or CRYPTO) - */ - private double mapPriceFeedServicePrice(double price, String currencyCode) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - return price; - } - return price == 0 ? 0 : 1 / price; - // TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore - } } diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 5e53ff64e4..bd3b1cc3ad 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -123,17 +123,14 @@ class CoreTradesService { BigInteger amount = amountAsLong == 0 ? offer.getAmount() : BigInteger.valueOf(amountAsLong); // adjust amount for fixed-price offer (based on TakeOfferViewModel) - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); + BigInteger maxAmount = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { - amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); - } else if (offer.isTraditionalOffer() - && !amount.equals(offer.getMinAmount()) && !amount.equals(amount)) { - // We only apply the rounding if the amount is variable (minAmount is lower as amount). - // Otherwise we could get an amount lower then the minAmount set by rounding - amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), maxTradeLimit, offer.getCurrencyCode(), offer.getPaymentMethodId()); + amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount); + } else if (offer.isTraditionalOffer() && offer.isRange()) { + amount = CoinUtil.getRoundedAmount(amount, offer.getPrice(), offer.getMinAmount(), maxAmount, offer.getCounterCurrencyCode(), offer.getPaymentMethodId()); } } @@ -192,7 +189,6 @@ class CoreTradesService { verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); - log.info("Keeping funds received from trade {}", tradeId); tradeManager.onTradeCompleted(trade); } diff --git a/core/src/main/java/haveno/core/api/CoreWalletsService.java b/core/src/main/java/haveno/core/api/CoreWalletsService.java index 68ec8c13ea..0433a8e994 100644 --- a/core/src/main/java/haveno/core/api/CoreWalletsService.java +++ b/core/src/main/java/haveno/core/api/CoreWalletsService.java @@ -173,12 +173,24 @@ class CoreWalletsService { } } - String relayXmrTx(String metadata) { + List createXmrSweepTxs(String address) { accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { - return xmrWalletService.relayTx(metadata); + return xmrWalletService.createSweepTxs(address); + } catch (Exception ex) { + log.error("", ex); + throw new IllegalStateException(ex); + } + } + + List relayXmrTxs(List metadatas) { + accountService.checkAccountOpen(); + verifyWalletsAreAvailable(); + verifyEncryptedWalletIsUnlocked(); + try { + return xmrWalletService.relayTxs(metadatas); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index dc38547df6..0f51f1be2b 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -24,6 +24,7 @@ import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; +import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; @@ -74,10 +75,13 @@ public final class XmrConnectionService { private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes + private static final int MAX_CONSECUTIVE_ERRORS = 3; // max errors before switching connections + private static int numConsecutiveErrors = 0; - public enum XmrConnectionError { + public enum XmrConnectionFallbackType { LOCAL, - CUSTOM + CUSTOM, + PROVIDED } private final Object lock = new Object(); @@ -92,12 +96,12 @@ public final class XmrConnectionService { private final MoneroConnectionManager connectionManager; private final EncryptedConnectionList connectionList; private final ObjectProperty> connections = new SimpleObjectProperty<>(); - private final IntegerProperty numConnections = new SimpleIntegerProperty(0); + private final IntegerProperty numConnections = new SimpleIntegerProperty(-1); private final ObjectProperty connectionProperty = new SimpleObjectProperty<>(); private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>(); + private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -105,15 +109,15 @@ public final class XmrConnectionService { private boolean isInitialized; private boolean pollInProgress; - private MoneroDaemonRpc daemon; + private MoneroDaemonRpc monerod; private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; private Long lastFallbackInvocation; private Long lastLogPollErrorTimestamp; - private long lastLogDaemonNotSyncedTimestamp; + private long lastLogMonerodNotSyncedTimestamp; private Long syncStartHeight; - private TaskLooper daemonPollLooper; + private TaskLooper monerodPollLooper; private long lastRefreshPeriodMs; @Getter private boolean isShutDownStarted; @@ -129,6 +133,7 @@ public final class XmrConnectionService { private Set 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, @@ -156,7 +161,13 @@ public final class XmrConnectionService { p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { - ThreadUtils.submitToPool(() -> initialize()); + ThreadUtils.submitToPool(() -> { + try { + initialize(); + } catch (Exception e) { + log.warn("Error initializing connection service, error={}\n", e.getMessage(), e); + } + }); } @Override public void onHiddenServicePublished() {} @@ -180,16 +191,16 @@ public final class XmrConnectionService { log.info("Shutting down {}", getClass().getSimpleName()); isInitialized = false; synchronized (lock) { - if (daemonPollLooper != null) daemonPollLooper.stop(); - daemon = null; + if (monerodPollLooper != null) monerodPollLooper.stop(); + monerod = null; } } // ------------------------ CONNECTION MANAGEMENT ------------------------- - public MoneroDaemonRpc getDaemon() { + public MoneroDaemonRpc getMonerod() { accountService.checkAccountOpen(); - return this.daemon; + return this.monerod; } public String getProxyUri() { @@ -270,7 +281,7 @@ public final class XmrConnectionService { accountService.checkAccountOpen(); // user needs to authorize fallback on startup after using locally synced node - if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { + if (fallbackRequiredBeforeConnectionSwitch()) { log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); return null; } @@ -283,6 +294,10 @@ public final class XmrConnectionService { return bestConnection; } + private boolean fallbackRequiredBeforeConnectionSwitch() { + return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored()); + } + private void addLocalNodeIfIgnored(Collection ignoredConnections) { if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); } @@ -390,7 +405,7 @@ public final class XmrConnectionService { } public void verifyConnection() { - if (daemon == null) throw new RuntimeException("No connection to Monero node"); + if (monerod == null) throw new RuntimeException("No connection to Monero node"); if (!Boolean.TRUE.equals(isConnected())) throw new RuntimeException("No connection to Monero node"); if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced"); } @@ -433,6 +448,7 @@ public final class XmrConnectionService { } public boolean hasSufficientPeersForBroadcast() { + if (numConnections.get() < 0) return true; // we don't know how many connections we have, but that's expected with restricted node return numConnections.get() >= getMinBroadcastConnections(); } @@ -458,15 +474,20 @@ public final class XmrConnectionService { public void fallbackToBestConnection() { if (isShutDownStarted) return; - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { + fallbackApplied = true; + if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) { log.warn("Falling back to public nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + initializeConnections(); } else { log.warn("Falling back to provided nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + initializeConnections(); + if (getConnection() == null) { + log.warn("No provided nodes available, falling back to public nodes"); + fallbackToBestConnection(); + } } - fallbackApplied = true; - initializeConnections(); } // ------------------------------- HELPERS -------------------------------- @@ -548,8 +569,8 @@ public final class XmrConnectionService { // register local node listener xmrLocalNode.addListener(new XmrLocalNodeListener() { @Override - public void onNodeStarted(MoneroDaemonRpc daemon) { - log.info("Local monero node started, height={}", daemon.getHeight()); + public void onNodeStarted(MoneroDaemonRpc monerod) { + log.info("Local monero node started, height={}", monerod.getHeight()); } @Override @@ -578,8 +599,8 @@ public final class XmrConnectionService { setConnection(connection.getUri()); // reset error connecting to local node - if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { - connectionServiceError.set(null); + if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) { + connectionServiceFallbackType.set(null); } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { MoneroRpcConnection bestConnection = getBestConnection(); @@ -602,8 +623,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.getAllXmrNodes()) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -615,8 +638,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -632,6 +657,11 @@ public final class XmrConnectionService { } } + // set if last node was locally syncing + if (!isInitialized) { + usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain(); + } + // set connection proxies log.info("TOR proxy URI: " + getProxyUri()); for (MoneroRpcConnection connection : connectionManager.getConnections()) { @@ -666,43 +696,30 @@ public final class XmrConnectionService { onConnectionChanged(connectionManager.getConnection()); } - private boolean lastUsedLocalSyncingNode() { - return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); - } - - public void startLocalNode() { + public void startLocalNode() throws Exception { // cannot start local node as seed node if (HavenoUtils.isSeedNode()) { throw new RuntimeException("Cannot start local node on seed node"); } - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - throw new RuntimeException(e); - } - } else { - throw new RuntimeException("Local node is not offline and used as last connection"); - } + // start local node + log.info("Starting local node"); + xmrLocalNode.start(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted || !accountService.isAccountOpen()) return; if (currentConnection == null) { - log.warn("Setting daemon connection to null", new Throwable("Stack trace")); + log.warn("Setting monerod connection to null", new Throwable("Stack trace")); } synchronized (lock) { if (currentConnection == null) { - daemon = null; + monerod = null; isConnected = false; connectionList.setCurrentConnectionUri(null); } else { - daemon = new MoneroDaemonRpc(currentConnection); + monerod = new MoneroDaemonRpc(currentConnection); isConnected = currentConnection.isConnected(); connectionList.removeConnection(currentConnection.getUri()); connectionList.addConnection(currentConnection); @@ -717,11 +734,11 @@ public final class XmrConnectionService { } // update key image poller - keyImagePoller.setDaemon(getDaemon()); + keyImagePoller.setMonerod(getMonerod()); keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); // update polling - doPollDaemon(); + doPollMonerod(); if (currentConnection != getConnection()) return; // polling can change connection UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000); @@ -741,74 +758,87 @@ public final class XmrConnectionService { private void startPolling() { synchronized (lock) { - if (daemonPollLooper != null) daemonPollLooper.stop(); - daemonPollLooper = new TaskLooper(() -> pollDaemon()); - daemonPollLooper.start(getInternalRefreshPeriodMs()); + if (monerodPollLooper != null) monerodPollLooper.stop(); + monerodPollLooper = new TaskLooper(() -> pollMonerod()); + monerodPollLooper.start(getInternalRefreshPeriodMs()); } } private void stopPolling() { synchronized (lock) { - if (daemonPollLooper != null) { - daemonPollLooper.stop(); - daemonPollLooper = null; + if (monerodPollLooper != null) { + monerodPollLooper.stop(); + monerodPollLooper = null; } } } - private void pollDaemon() { + private void pollMonerod() { if (pollInProgress) return; - doPollDaemon(); + doPollMonerod(); } - private void doPollDaemon() { + private void doPollMonerod() { synchronized (pollLock) { pollInProgress = true; if (isShutDownStarted) return; try { - // poll daemon - if (daemon == null) switchToBestConnection(); + // poll monerod + if (monerod == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection(); try { - if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); - lastInfo = daemon.getInfo(); + if (monerod == null) throw new RuntimeException("No connection to Monero daemon"); + lastInfo = monerod.getInfo(); + numConsecutiveErrors = 0; } catch (Exception e) { // skip handling if shutting down if (isShutDownStarted) return; + // skip error handling up to max attempts + numConsecutiveErrors++; + if (numConsecutiveErrors <= MAX_CONSECUTIVE_ERRORS) { + return; + } else { + numConsecutiveErrors = 0; // reset error count + } + // invoke fallback handling on startup error - boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); + boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup; if (lastInfo == null && canFallback) { - if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); - if (lastUsedLocalSyncingNode()) { - log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.LOCAL); + if (usedSyncingLocalNodeBeforeStartup) { + log.warn("Failed to fetch monerod info from local connection on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL); + } else if (isProvidedConnections()) { + log.warn("Failed to fetch monerod info from provided connections on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED); } else { - log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.CUSTOM); + log.warn("Failed to fetch monerod info from custom connection on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM); } } return; } // log error message periodically - if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { - log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage()); + if (lastWarningOutsidePeriod()) { + MoneroRpcConnection connection = getConnection(); + log.warn("Error fetching daemon info after max attempts. Trying to switch to best connection. monerod={}, error={}", connection == null ? "null" : connection.getUri(), e.getMessage()); if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e)); lastLogPollErrorTimestamp = System.currentTimeMillis(); } // switch to best connection switchToBestConnection(); - if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling"); - lastInfo = daemon.getInfo(); // caught internally if still fails + if (monerod == null) throw new RuntimeException("No connection to Monero daemon after error handling"); + lastInfo = monerod.getInfo(); // caught internally if still fails } - // connected to daemon + // connected to monerod isConnected = true; - connectionServiceError.set(null); + 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 @@ -816,10 +846,10 @@ public final class XmrConnectionService { // write sync status to preferences preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing); - // throttle warnings if daemon not synced - if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) { + // throttle warnings if monerod not synced + if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogMonerodNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) { log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight()); - lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis(); + lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis(); } // announce connection change if refresh period changes @@ -829,6 +859,9 @@ public final class XmrConnectionService { return; } + // get the number of connections, which is only available if not restricted + int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections(); + // update properties on user thread UserThread.execute(() -> { @@ -854,15 +887,22 @@ public final class XmrConnectionService { } } connections.set(availableConnections); - numConnections.set(availableConnections.size()); + numConnections.set(numOutgoingConnections); // notify update numUpdates.set(numUpdates.get() + 1); }); + // invoke error handling if no connections + if (numOutgoingConnections == 0) { + String errorMsg = "The Monero node has no connected peers. It may be experiencing a network connectivity issue."; + log.warn(errorMsg); + throw new RuntimeException(errorMsg); + } + // handle error recovery if (lastLogPollErrorTimestamp != null) { - log.info("Successfully fetched daemon info after previous error"); + log.info("Successfully fetched monerod info after previous error"); lastLogPollErrorTimestamp = null; } @@ -870,25 +910,40 @@ public final class XmrConnectionService { getConnectionServiceErrorMsg().set(null); } catch (Exception e) { - // not connected to daemon + // not connected to monerod isConnected = false; // skip if shut down if (isShutDownStarted) return; + // format error message + String errorMsg = e.getMessage(); + if (errorMsg != null && errorMsg.contains(": ")) { + errorMsg = errorMsg.substring(errorMsg.indexOf(": ") + 2); // strip exception class + } + errorMsg = Res.get("popup.warning.moneroConnection", errorMsg); + // set error message - getConnectionServiceErrorMsg().set(e.getMessage()); + getConnectionServiceErrorMsg().set(errorMsg); } finally { pollInProgress = false; } } } + private boolean lastWarningOutsidePeriod() { + return lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS; + } + private boolean isFixedConnection() { - return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; + return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } private boolean isCustomConnections() { return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } + + private boolean isProvidedConnections() { + return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED; + } } diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 7295202c64..0928340d25 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -109,17 +109,18 @@ public class XmrLocalNode { public boolean shouldBeIgnored() { if (config.ignoreLocalXmrNode) return true; - // determine if local node is configured + // ignore if fixed connection is not local + if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode); + + // check if local node is within configuration boolean hasConfiguredLocalNode = false; for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { - if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { + if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) { hasConfiguredLocalNode = true; break; } } - if (!hasConfiguredLocalNode) return true; - - return false; + return !hasConfiguredLocalNode; } public void addListener(XmrLocalNodeListener listener) { diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java index 76de24401a..5de9aa84a3 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -129,7 +129,7 @@ public class OfferInfo implements Payload { public static OfferInfo toMyOfferInfo(OpenOffer openOffer) { // An OpenOffer is always my offer. var offer = openOffer.getOffer(); - var currencyCode = offer.getCurrencyCode(); + var currencyCode = offer.getCounterCurrencyCode(); var isActivated = !openOffer.isDeactivated(); Optional optionalTriggerPrice = openOffer.getTriggerPrice() > 0 ? Optional.of(Price.valueOf(currencyCode, openOffer.getTriggerPrice())) @@ -150,7 +150,7 @@ public class OfferInfo implements Payload { private static OfferInfoBuilder getBuilder(Offer offer) { // OfferInfo protos are passed to API client, and some field // values are converted to displayable, unambiguous form. - var currencyCode = offer.getCurrencyCode(); + var currencyCode = offer.getCounterCurrencyCode(); var preciseOfferPrice = reformatMarketPrice( requireNonNull(offer.getPrice()).toPlainString(), currencyCode); diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java index 6b1b494047..f7dc4a5063 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -78,7 +78,15 @@ public final class PaymentAccountForm implements PersistablePayload { CASH_APP, PAYPAL, VENMO, - PAYSAFE; + PAYSAFE, + WECHAT_PAY, + ALI_PAY, + SWISH, + TRANSFERWISE_USD, + AMAZON_GIFT_CARD, + ACH_TRANSFER, + INTERAC_E_TRANSFER, + US_POSTAL_MONEY_ORDER; public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) { return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name()); diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index 8df26368ba..ab86a05240 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -57,7 +57,7 @@ public class TradeInfo implements Payload { private static final Function toPreciseTradePrice = (trade) -> reformatMarketPrice(requireNonNull(trade.getPrice()).toPlainString(), - trade.getOffer().getCurrencyCode()); + trade.getOffer().getCounterCurrencyCode()); // Haveno v1 trade protocol fields (some are in common with the BSQ Swap protocol). private final OfferInfo offer; @@ -91,14 +91,19 @@ public class TradeInfo implements Payload { private final boolean isDepositsPublished; private final boolean isDepositsConfirmed; private final boolean isDepositsUnlocked; + private final boolean isDepositsFinalized; private final boolean isPaymentSent; private final boolean isPaymentReceived; private final boolean isPayoutPublished; private final boolean isPayoutConfirmed; private final boolean isPayoutUnlocked; + private final boolean isPayoutFinalized; private final boolean isCompleted; private final String contractAsJson; private final ContractInfo contract; + private final long startTime; + private final long maxDurationMs; + private final long deadlineTime; public TradeInfo(TradeInfoV1Builder builder) { this.offer = builder.getOffer(); @@ -132,14 +137,19 @@ public class TradeInfo implements Payload { this.isDepositsPublished = builder.isDepositsPublished(); this.isDepositsConfirmed = builder.isDepositsConfirmed(); this.isDepositsUnlocked = builder.isDepositsUnlocked(); + this.isDepositsFinalized = builder.isDepositsFinalized(); this.isPaymentSent = builder.isPaymentSent(); this.isPaymentReceived = builder.isPaymentReceived(); this.isPayoutPublished = builder.isPayoutPublished(); this.isPayoutConfirmed = builder.isPayoutConfirmed(); this.isPayoutUnlocked = builder.isPayoutUnlocked(); + this.isPayoutFinalized = builder.isPayoutFinalized(); this.isCompleted = builder.isCompleted(); this.contractAsJson = builder.getContractAsJson(); this.contract = builder.getContract(); + this.startTime = builder.getStartTime(); + this.maxDurationMs = builder.getMaxDurationMs(); + this.deadlineTime = builder.getDeadlineTime(); } public static TradeInfo toTradeInfo(Trade trade) { @@ -193,15 +203,20 @@ public class TradeInfo implements Payload { .withIsDepositsPublished(trade.isDepositsPublished()) .withIsDepositsConfirmed(trade.isDepositsConfirmed()) .withIsDepositsUnlocked(trade.isDepositsUnlocked()) + .withIsDepositsFinalized(trade.isDepositsFinalized()) .withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentReceived(trade.isPaymentReceived()) .withIsPayoutPublished(trade.isPayoutPublished()) .withIsPayoutConfirmed(trade.isPayoutConfirmed()) .withIsPayoutUnlocked(trade.isPayoutUnlocked()) + .withIsPayoutFinalized(trade.isPayoutFinalized()) .withIsCompleted(trade.isCompleted()) .withContractAsJson(trade.getContractAsJson()) .withContract(contractInfo) .withOffer(toOfferInfo(trade.getOffer())) + .withStartTime(trade.getStartDate().getTime()) + .withMaxDurationMs(trade.getMaxTradePeriod()) + .withDeadlineTime(trade.getMaxTradePeriodDate().getTime()) .build(); } @@ -243,14 +258,19 @@ public class TradeInfo implements Payload { .setIsDepositsPublished(isDepositsPublished) .setIsDepositsConfirmed(isDepositsConfirmed) .setIsDepositsUnlocked(isDepositsUnlocked) + .setIsDepositsFinalized(isDepositsFinalized) .setIsPaymentSent(isPaymentSent) .setIsPaymentReceived(isPaymentReceived) .setIsCompleted(isCompleted) .setIsPayoutPublished(isPayoutPublished) .setIsPayoutConfirmed(isPayoutConfirmed) .setIsPayoutUnlocked(isPayoutUnlocked) + .setIsPayoutFinalized(isPayoutFinalized) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) + .setStartTime(startTime) + .setMaxDurationMs(maxDurationMs) + .setDeadlineTime(deadlineTime) .build(); } @@ -287,14 +307,19 @@ public class TradeInfo implements Payload { .withIsDepositsPublished(proto.getIsDepositsPublished()) .withIsDepositsConfirmed(proto.getIsDepositsConfirmed()) .withIsDepositsUnlocked(proto.getIsDepositsUnlocked()) + .withIsDepositsFinalized(proto.getIsDepositsFinalized()) .withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsCompleted(proto.getIsCompleted()) .withIsPayoutPublished(proto.getIsPayoutPublished()) .withIsPayoutConfirmed(proto.getIsPayoutConfirmed()) .withIsPayoutUnlocked(proto.getIsPayoutUnlocked()) + .withIsPayoutFinalized(proto.getIsPayoutFinalized()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) + .withStartTime(proto.getStartTime()) + .withMaxDurationMs(proto.getMaxDurationMs()) + .withDeadlineTime(proto.getDeadlineTime()) .build(); } @@ -330,15 +355,20 @@ public class TradeInfo implements Payload { ", isDepositsPublished=" + isDepositsPublished + "\n" + ", isDepositsConfirmed=" + isDepositsConfirmed + "\n" + ", isDepositsUnlocked=" + isDepositsUnlocked + "\n" + + ", isDepositsFinalized=" + isDepositsFinalized + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPayoutPublished=" + isPayoutPublished + "\n" + ", isPayoutConfirmed=" + isPayoutConfirmed + "\n" + ", isPayoutUnlocked=" + isPayoutUnlocked + "\n" + + ", isPayoutFinalized=" + isPayoutFinalized + "\n" + ", isCompleted=" + isCompleted + "\n" + ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + + ", startTime=" + startTime + "\n" + + ", maxDurationMs=" + maxDurationMs + "\n" + + ", deadlineTime=" + deadlineTime + "\n" + '}'; } } diff --git a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java index dcf40b8a69..ec751e0d5b 100644 --- a/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java @@ -64,15 +64,20 @@ public final class TradeInfoV1Builder { private boolean isDepositsPublished; private boolean isDepositsConfirmed; private boolean isDepositsUnlocked; + private boolean isDepositsFinalized; private boolean isPaymentSent; private boolean isPaymentReceived; private boolean isPayoutPublished; private boolean isPayoutConfirmed; private boolean isPayoutUnlocked; + private boolean isPayoutFinalized; private boolean isCompleted; private String contractAsJson; private ContractInfo contract; private String closingStatus; + private long startTime; + private long maxDurationMs; + private long deadlineTime; public TradeInfoV1Builder withOffer(OfferInfo offer) { this.offer = offer; @@ -239,6 +244,11 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) { + this.isDepositsFinalized = isDepositsFinalized; + return this; + } + public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) { this.isPaymentSent = isPaymentSent; return this; @@ -264,6 +274,11 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) { + this.isPayoutFinalized = isPayoutFinalized; + return this; + } + public TradeInfoV1Builder withIsCompleted(boolean isCompleted) { this.isCompleted = isCompleted; return this; @@ -284,6 +299,21 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withStartTime(long startTime) { + this.startTime = startTime; + return this; + } + + public TradeInfoV1Builder withMaxDurationMs(long maxDurationMs) { + this.maxDurationMs = maxDurationMs; + return this; + } + + public TradeInfoV1Builder withDeadlineTime(long deadlineTime) { + this.deadlineTime = deadlineTime; + return this; + } + public TradeInfo build() { return new TradeInfo(this); } diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 84bdcc746a..0bdac1abc1 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); - havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 19503fafd8..192e3870b7 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; -import haveno.core.api.XmrConnectionService.XmrConnectionError; +import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -159,7 +159,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionErrorHandler; + private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -431,9 +431,9 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceError().addListener((observable, oldValue, newValue) -> { - if (displayMoneroConnectionErrorHandler == null) return; - displayMoneroConnectionErrorHandler.accept(newValue); + getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); }); log.info("Init P2P network"); @@ -735,8 +735,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public ObjectProperty getConnectionServiceError() { - return xmrConnectionService.getConnectionServiceError(); + public ObjectProperty getConnectionServiceFallbackType() { + return xmrConnectionService.getConnectionServiceFallbackType(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java index 69609a4bba..18a26bdb5e 100644 --- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java @@ -94,7 +94,7 @@ public class P2PNetworkSetup { if (warning != null && p2pPeers == 0) { result = warning; } else { - String p2pInfo = ((int) numXmrPeers > 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers); + String p2pInfo = ((int) numXmrPeers >= 0 ? Res.get("mainView.footer.xmrPeers", numXmrPeers) + " / " : "") + Res.get("mainView.footer.p2pPeers", numP2pPeers); if (dataReceived && hiddenService) { result = p2pInfo; } else if (p2pPeers == 0) diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 7d37372afd..6bf3b71f8b 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -188,6 +188,8 @@ public class WalletAppSetup { } else { xmrConnectionService.getConnectionServiceErrorMsg().set(Res.get("mainView.walletServiceErrorMsg.connectionError", exception.getMessage())); } + } else { + xmrConnectionService.getConnectionServiceErrorMsg().set(errorMsg); } } return result; diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 9c8016d506..3184d9ba11 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -151,23 +151,23 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }); - - // shut down trade and wallet services - log.info("Shutting down trade and wallet services"); - injector.getInstance(OfferBookService.class).shutDown(); - injector.getInstance(TradeManager.class).shutDown(); - injector.getInstance(BtcWalletService.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(); - injector.getInstance(XmrConnectionService.class).shutDown(); - injector.getInstance(WalletsSetup.class).shutDown(); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(XmrConnectionService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); // we wait max 5 sec. UserThread.runAfter(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); - log.info("Graceful shutdown caused a timeout. Exiting now."); + log.warn("Graceful shutdown caused a timeout. Exiting now."); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }, 5); diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java index 2cebb66f3a..dd55bbc96a 100644 --- a/core/src/main/java/haveno/core/filter/FilterManager.java +++ b/core/src/main/java/haveno/core/filter/FilterManager.java @@ -84,9 +84,9 @@ public class FilterManager { private final ConfigFileEditor configFileEditor; private final ProvidersRepository providersRepository; private final boolean ignoreDevMsg; + private final boolean useDevPrivilegeKeys; private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); private final List listeners = new CopyOnWriteArrayList<>(); - private final List publicKeys; private ECKey filterSigningKey; private final Set invalidFilters = new HashSet<>(); private Consumer filterWarningHandler; @@ -113,16 +113,31 @@ public class FilterManager { this.configFileEditor = new ConfigFileEditor(config.configFile); this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; - - publicKeys = useDevPrivilegeKeys ? - Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : - List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", - "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", - "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); + this.useDevPrivilegeKeys = useDevPrivilegeKeys; banFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork); } + protected List getPubKeyList() { + switch (Config.baseCurrencyNetwork()) { + case XMR_LOCAL: + if (useDevPrivilegeKeys) return Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY); + return List.of( + "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee", + "024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492", + "026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519"); + case XMR_STAGENET: + return List.of( + "03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859", + "02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba", + "0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de"); + case XMR_MAINNET: + return List.of(); + default: + throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // API @@ -587,16 +602,16 @@ public class FilterManager { "but the new version does not recognize it as valid filter): " + "signerPubKeyAsHex from filter is not part of our pub key list. " + "signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}", - signerPubKeyAsHex, publicKeys, new Date(filter.getCreationDate())); + signerPubKeyAsHex, getPubKeyList(), new Date(filter.getCreationDate())); return false; } return true; } private boolean isPublicKeyInList(String pubKeyAsHex) { - boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); + boolean isPublicKeyInList = getPubKeyList().contains(pubKeyAsHex); if (!isPublicKeyInList) { - log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); + log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, getPubKeyList()); } return isPublicKeyInList; } diff --git a/core/src/main/java/haveno/core/locale/CryptoCurrency.java b/core/src/main/java/haveno/core/locale/CryptoCurrency.java index 6c46c9d2b3..feabaf1943 100644 --- a/core/src/main/java/haveno/core/locale/CryptoCurrency.java +++ b/core/src/main/java/haveno/core/locale/CryptoCurrency.java @@ -54,7 +54,7 @@ public final class CryptoCurrency extends TradeCurrency { public static CryptoCurrency fromProto(protobuf.TradeCurrency proto) { return new CryptoCurrency(proto.getCode(), - proto.getName(), + CurrencyUtil.getNameByCode(proto.getCode()), proto.getCryptoCurrency().getIsAsset()); } diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 6ed42b6234..7f268597be 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -66,7 +66,7 @@ import static java.lang.String.format; @Slf4j public class CurrencyUtil { public static void setup() { - setBaseCurrencyCode("XMR"); + setBaseCurrencyCode(baseCurrencyCode); } private static final AssetRegistry assetRegistry = new AssetRegistry(); @@ -93,7 +93,7 @@ public class CurrencyUtil { public static Collection getAllSortedFiatCurrencies(Comparator comparator) { return getAllSortedTraditionalCurrencies(comparator).stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) - .collect(Collectors.toList()); // sorted by currency name + .collect(Collectors.toList()); // sorted by currency name } public static List getAllFiatCurrencies() { @@ -105,11 +105,11 @@ public class CurrencyUtil { public static List getAllSortedFiatCurrencies() { return getAllSortedTraditionalCurrencies().stream() .filter(currency -> CurrencyUtil.isFiatCurrency(currency.getCode())) - .collect(Collectors.toList()); // sorted by currency name + .collect(Collectors.toList()); // sorted by currency name } public static Collection getAllSortedTraditionalCurrencies() { - return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name + return traditionalCurrencyMapSupplier.get().values(); // sorted by currency name } public static List getAllTraditionalCurrencies() { @@ -198,12 +198,16 @@ public class CurrencyUtil { final List result = new ArrayList<>(); result.add(new CryptoCurrency("BTC", "Bitcoin")); result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); + result.add(new CryptoCurrency("DOGE", "Dogecoin")); result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); - result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)")); - result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); - result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); - result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); + result.add(new CryptoCurrency("XRP", "Ripple")); + result.add(new CryptoCurrency("ADA", "Cardano")); + result.add(new CryptoCurrency("SOL", "Solana")); + result.add(new CryptoCurrency("TRX", "Tron")); + result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin")); + result.add(new CryptoCurrency("USDT-ERC20", "Tether USD")); + result.add(new CryptoCurrency("USDC-ERC20", "USD Coin")); result.sort(TradeCurrency::compareTo); return result; } @@ -284,7 +288,7 @@ public class CurrencyUtil { } /** - * We return true if it is BTC or any of our currencies available in the assetRegistry. + * We return true if it is XMR or any of our currencies available in the assetRegistry. * For removed assets it would fail as they are not found but we don't want to conclude that they are traditional then. * As the caller might not deal with the case that a currency can be neither a cryptoCurrency nor Traditional if not found * we return true as well in case we have no traditional currency for the code. @@ -406,6 +410,13 @@ public class CurrencyUtil { removedCryptoCurrency.isPresent() ? removedCryptoCurrency.get().getName() : Res.get("shared.na"); return getCryptoCurrency(currencyCode).map(TradeCurrency::getName).orElse(xmrOrRemovedAsset); } + if (isTraditionalNonFiatCurrency(currencyCode)) { + return getTraditionalNonFiatCurrencies().stream() + .filter(currency -> currency.getCode().equals(currencyCode)) + .findAny() + .map(TradeCurrency::getName) + .orElse(currencyCode); + } try { return Currency.getInstance(currencyCode).getDisplayName(); } catch (Throwable t) { @@ -507,17 +518,11 @@ public class CurrencyUtil { } public static String getCurrencyPair(String currencyCode) { - if (isTraditionalCurrency(currencyCode)) - return Res.getBaseCurrencyCode() + "/" + currencyCode; - else - return currencyCode + "/" + Res.getBaseCurrencyCode(); + return Res.getBaseCurrencyCode() + "/" + currencyCode; } public static String getCounterCurrency(String currencyCode) { - if (isTraditionalCurrency(currencyCode)) - return currencyCode; - else - return Res.getBaseCurrencyCode(); + return currencyCode; } public static String getPriceWithCurrencyCode(String currencyCode) { @@ -525,10 +530,7 @@ public class CurrencyUtil { } public static String getPriceWithCurrencyCode(String currencyCode, String translationKey) { - if (isCryptoCurrency(currencyCode)) - return Res.get(translationKey, Res.getBaseCurrencyCode(), currencyCode); - else - return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode()); + return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode()); } public static String getOfferVolumeCode(String currencyCode) { diff --git a/core/src/main/java/haveno/core/locale/Res.java b/core/src/main/java/haveno/core/locale/Res.java index e44092561f..3ea4d1c5ac 100644 --- a/core/src/main/java/haveno/core/locale/Res.java +++ b/core/src/main/java/haveno/core/locale/Res.java @@ -103,7 +103,11 @@ public class Res { } public static String get(String key, Object... arguments) { - return MessageFormat.format(Res.get(key), arguments); + return MessageFormat.format(escapeQuotes(get(key)), arguments); + } + + private static String escapeQuotes(String s) { + return s.replace("'", "''"); } public static String get(String key) { diff --git a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java index 1ab491467e..cc42342abb 100644 --- a/core/src/main/java/haveno/core/locale/TraditionalCurrency.java +++ b/core/src/main/java/haveno/core/locale/TraditionalCurrency.java @@ -86,7 +86,7 @@ public final class TraditionalCurrency extends TradeCurrency { } public static TraditionalCurrency fromProto(protobuf.TradeCurrency proto) { - return new TraditionalCurrency(proto.getCode(), proto.getName()); + return new TraditionalCurrency(proto.getCode(), CurrencyUtil.getNameByCode(proto.getCode())); } diff --git a/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java b/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java index 0e4777488a..257d80b515 100644 --- a/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java +++ b/core/src/main/java/haveno/core/monetary/CryptoExchangeRate.java @@ -32,7 +32,7 @@ public class CryptoExchangeRate { */ public final Coin coin; - public final CryptoMoney crypto; + public final CryptoMoney cryptoMoney; /** * Construct exchange rate. This amount of coin is worth that amount of crypto. @@ -43,7 +43,7 @@ public class CryptoExchangeRate { checkArgument(crypto.isPositive()); checkArgument(crypto.currencyCode != null, "currency code required"); this.coin = coin; - this.crypto = crypto; + this.cryptoMoney = crypto; } /** @@ -59,13 +59,13 @@ public class CryptoExchangeRate { * @throws ArithmeticException if the converted crypto amount is too high or too low. */ public CryptoMoney coinToCrypto(Coin convertCoin) { - BigInteger converted = BigInteger.valueOf(coin.value) - .multiply(BigInteger.valueOf(convertCoin.value)) - .divide(BigInteger.valueOf(crypto.value)); + final BigInteger converted = BigInteger.valueOf(convertCoin.value) + .multiply(BigInteger.valueOf(cryptoMoney.value)) + .divide(BigInteger.valueOf(coin.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); - return CryptoMoney.valueOf(crypto.currencyCode, converted.longValue()); + return CryptoMoney.valueOf(cryptoMoney.currencyCode, converted.longValue()); } /** @@ -74,12 +74,11 @@ public class CryptoExchangeRate { * @throws ArithmeticException if the converted coin amount is too high or too low. */ public Coin cryptoToCoin(CryptoMoney convertCrypto) { - checkArgument(convertCrypto.currencyCode.equals(crypto.currencyCode), "Currency mismatch: %s vs %s", - convertCrypto.currencyCode, crypto.currencyCode); + checkArgument(convertCrypto.currencyCode.equals(cryptoMoney.currencyCode), "Currency mismatch: %s vs %s", + convertCrypto.currencyCode, cryptoMoney.currencyCode); // Use BigInteger because it's much easier to maintain full precision without overflowing. - BigInteger converted = BigInteger.valueOf(crypto.value) - .multiply(BigInteger.valueOf(convertCrypto.value)) - .divide(BigInteger.valueOf(coin.value)); + final BigInteger converted = BigInteger.valueOf(convertCrypto.value).multiply(BigInteger.valueOf(coin.value)) + .divide(BigInteger.valueOf(cryptoMoney.value)); if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) throw new ArithmeticException("Overflow"); diff --git a/core/src/main/java/haveno/core/monetary/Price.java b/core/src/main/java/haveno/core/monetary/Price.java index 8dccd0608d..2ea1481ee8 100644 --- a/core/src/main/java/haveno/core/monetary/Price.java +++ b/core/src/main/java/haveno/core/monetary/Price.java @@ -136,7 +136,7 @@ public class Price extends MonetaryWrapper implements Comparable { public String toFriendlyString() { return monetary instanceof CryptoMoney ? - ((CryptoMoney) monetary).toFriendlyString() + "/XMR" : + ((CryptoMoney) monetary).toFriendlyString().replace(((CryptoMoney) monetary).currencyCode, "") + "XMR/" + ((CryptoMoney) monetary).currencyCode : ((TraditionalMoney) monetary).toFriendlyString().replace(((TraditionalMoney) monetary).currencyCode, "") + "XMR/" + ((TraditionalMoney) monetary).currencyCode; } diff --git a/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java b/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java index 976d3fa2ec..48f1eb20d5 100644 --- a/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java +++ b/core/src/main/java/haveno/core/monetary/TraditionalExchangeRate.java @@ -15,84 +15,84 @@ * along with Bisq. If not, see . */ - package haveno.core.monetary; +package haveno.core.monetary; - import static com.google.common.base.Preconditions.checkArgument; - - import java.io.Serializable; - import java.math.BigInteger; - - import org.bitcoinj.core.Coin; - - import com.google.common.base.Objects; - - /** - * An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount. - */ - public class TraditionalExchangeRate implements Serializable { - - public final Coin coin; - public final TraditionalMoney traditionalMoney; - - /** Construct exchange rate. This amount of coin is worth that amount of money. */ - public TraditionalExchangeRate(Coin coin, TraditionalMoney traditionalMoney) { - checkArgument(coin.isPositive()); - checkArgument(traditionalMoney.isPositive()); - checkArgument(traditionalMoney.currencyCode != null, "currency code required"); - this.coin = coin; - this.traditionalMoney = traditionalMoney; - } - - /** Construct exchange rate. One coin is worth this amount of traditional money. */ - public TraditionalExchangeRate(TraditionalMoney traditionalMoney) { - this(Coin.COIN, traditionalMoney); - } - - /** - * Convert a coin amount to a traditional money amount using this exchange rate. - * @throws ArithmeticException if the converted amount is too high or too low. - */ - public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) { - // Use BigInteger because it's much easier to maintain full precision without overflowing. - final BigInteger converted = BigInteger.valueOf(convertCoin.value).multiply(BigInteger.valueOf(traditionalMoney.value)) - .divide(BigInteger.valueOf(coin.value)); - if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 - || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new ArithmeticException("Overflow"); - return TraditionalMoney.valueOf(traditionalMoney.currencyCode, converted.longValue()); - } - - /** - * Convert a traditional money amount to a coin amount using this exchange rate. - * @throws ArithmeticException if the converted coin amount is too high or too low. - */ - public Coin traditionalMoneyToCoin(TraditionalMoney convertTraditionalMoney) { - checkArgument(convertTraditionalMoney.currencyCode.equals(traditionalMoney.currencyCode), "Currency mismatch: %s vs %s", - convertTraditionalMoney.currencyCode, traditionalMoney.currencyCode); - // Use BigInteger because it's much easier to maintain full precision without overflowing. - final BigInteger converted = BigInteger.valueOf(convertTraditionalMoney.value).multiply(BigInteger.valueOf(coin.value)) - .divide(BigInteger.valueOf(traditionalMoney.value)); - if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 - || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new ArithmeticException("Overflow"); - try { - return Coin.valueOf(converted.longValue()); - } catch (IllegalArgumentException x) { - throw new ArithmeticException("Overflow: " + x.getMessage()); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TraditionalExchangeRate other = (TraditionalExchangeRate) o; - return Objects.equal(this.coin, other.coin) && Objects.equal(this.traditionalMoney, other.traditionalMoney); - } - - @Override - public int hashCode() { - return Objects.hashCode(coin, traditionalMoney); - } - } - \ No newline at end of file +import static com.google.common.base.Preconditions.checkArgument; + +import java.io.Serializable; +import java.math.BigInteger; + +import org.bitcoinj.core.Coin; + +import com.google.common.base.Objects; + +/** + * An exchange rate is expressed as a ratio of a {@link Coin} and a traditional money amount. +*/ +public class TraditionalExchangeRate implements Serializable { + + public final Coin coin; + public final TraditionalMoney traditionalMoney; + + /** Construct exchange rate. This amount of coin is worth that amount of money. */ + public TraditionalExchangeRate(Coin coin, TraditionalMoney traditionalMoney) { + checkArgument(coin.isPositive()); + checkArgument(traditionalMoney.isPositive()); + checkArgument(traditionalMoney.currencyCode != null, "currency code required"); + this.coin = coin; + this.traditionalMoney = traditionalMoney; + } + + /** Construct exchange rate. One coin is worth this amount of traditional money. */ + public TraditionalExchangeRate(TraditionalMoney traditionalMoney) { + this(Coin.COIN, traditionalMoney); + } + + /** + * Convert a coin amount to a traditional money amount using this exchange rate. + * @throws ArithmeticException if the converted amount is too high or too low. + */ + public TraditionalMoney coinToTraditionalMoney(Coin convertCoin) { + // Use BigInteger because it's much easier to maintain full precision without overflowing. + final BigInteger converted = BigInteger.valueOf(convertCoin.value) + .multiply(BigInteger.valueOf(traditionalMoney.value)) + .divide(BigInteger.valueOf(coin.value)); + if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) + throw new ArithmeticException("Overflow"); + return TraditionalMoney.valueOf(traditionalMoney.currencyCode, converted.longValue()); + } + + /** + * Convert a traditional money amount to a coin amount using this exchange rate. + * @throws ArithmeticException if the converted coin amount is too high or too low. + */ + public Coin traditionalMoneyToCoin(TraditionalMoney convertTraditionalMoney) { + checkArgument(convertTraditionalMoney.currencyCode.equals(traditionalMoney.currencyCode), "Currency mismatch: %s vs %s", + convertTraditionalMoney.currencyCode, traditionalMoney.currencyCode); + // Use BigInteger because it's much easier to maintain full precision without overflowing. + final BigInteger converted = BigInteger.valueOf(convertTraditionalMoney.value).multiply(BigInteger.valueOf(coin.value)) + .divide(BigInteger.valueOf(traditionalMoney.value)); + if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 + || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) + throw new ArithmeticException("Overflow"); + try { + return Coin.valueOf(converted.longValue()); + } catch (IllegalArgumentException x) { + throw new ArithmeticException("Overflow: " + x.getMessage()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TraditionalExchangeRate other = (TraditionalExchangeRate) o; + return Objects.equal(this.coin, other.coin) && Objects.equal(this.traditionalMoney, other.traditionalMoney); + } + + @Override + public int hashCode() { + return Objects.hashCode(coin, traditionalMoney); + } +} diff --git a/core/src/main/java/haveno/core/network/MessageState.java b/core/src/main/java/haveno/core/network/MessageState.java index c4ae7f8f40..759b87d0a2 100644 --- a/core/src/main/java/haveno/core/network/MessageState.java +++ b/core/src/main/java/haveno/core/network/MessageState.java @@ -23,5 +23,6 @@ public enum MessageState { ARRIVED, STORED_IN_MAILBOX, ACKNOWLEDGED, - FAILED + FAILED, + NACKED } diff --git a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java index 0dd6fe58ac..04554c7c2c 100644 --- a/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/haveno/core/notifications/alerts/TradeEvents.java @@ -59,7 +59,7 @@ public class TradeEvents { } private void setTradePhaseListener(Trade trade) { - if (isInitialized) log.info("We got a new trade. id={}", trade.getId()); + if (isInitialized) log.info("We got a new trade, tradeId={}", trade.getId(), "hasBuyerAsTakerWithoutDeposit=" + trade.getOffer().hasBuyerAsTakerWithoutDeposit()); if (!trade.isPayoutPublished()) { trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { String msg = null; @@ -70,6 +70,7 @@ public class TradeEvents { case DEPOSITS_PUBLISHED: break; case DEPOSITS_UNLOCKED: + case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized? if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.conf", shortId); break; diff --git a/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java index af2434dd22..bb29a2e678 100644 --- a/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java +++ b/core/src/main/java/haveno/core/notifications/alerts/market/MarketAlerts.java @@ -110,13 +110,12 @@ public class MarketAlerts { } private void onOfferAdded(Offer offer) { - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); Price offerPrice = offer.getPrice(); if (marketPrice != null && offerPrice != null) { boolean isSellOffer = offer.getDirection() == OfferDirection.SELL; String shortOfferId = offer.getShortId(); - boolean isTraditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode); String alertId = getAlertId(offer); user.getMarketAlertFilters().stream() .filter(marketAlertFilter -> !offer.isMyOffer(keyRing)) @@ -133,9 +132,7 @@ public class MarketAlerts { double offerPriceValue = offerPrice.getValue(); double ratio = offerPriceValue / marketPriceAsDouble; ratio = 1 - ratio; - if (isTraditionalCurrency && isSellOffer) - ratio *= -1; - else if (!isTraditionalCurrency && !isSellOffer) + if (isSellOffer) ratio *= -1; ratio = ratio * 10000; @@ -148,26 +145,14 @@ public class MarketAlerts { if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) { String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy"); String marketDir; - if (isTraditionalCurrency) { - if (isSellOffer) { - marketDir = ratio > 0 ? - Res.get("account.notifications.marketAlert.message.msg.above") : - Res.get("account.notifications.marketAlert.message.msg.below"); - } else { - marketDir = ratio < 0 ? - Res.get("account.notifications.marketAlert.message.msg.above") : - Res.get("account.notifications.marketAlert.message.msg.below"); - } + if (isSellOffer) { + marketDir = ratio > 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); } else { - if (isSellOffer) { - marketDir = ratio < 0 ? - Res.get("account.notifications.marketAlert.message.msg.above") : - Res.get("account.notifications.marketAlert.message.msg.below"); - } else { - marketDir = ratio > 0 ? - Res.get("account.notifications.marketAlert.message.msg.above") : - Res.get("account.notifications.marketAlert.message.msg.below"); - } + marketDir = ratio < 0 ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); } ratio = Math.abs(ratio); diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index fab646433b..d0a79be77c 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -22,7 +22,6 @@ import com.google.inject.Singleton; import haveno.common.app.Version; import haveno.common.crypto.PubKeyRingProvider; import haveno.common.util.Utilities; -import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.payment.PaymentAccount; @@ -35,6 +34,7 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; @@ -92,7 +92,6 @@ public class CreateOfferService { Version.VERSION.replace(".", ""); } - // TODO: add trigger price? public Offer createAndGetOffer(String offerId, OfferDirection direction, String currencyCode, @@ -134,10 +133,12 @@ public class CreateOfferService { // must nullify empty string so contracts match if ("".equals(extraInfo)) extraInfo = null; - // verify buyer as taker security deposit + // verify config for private no deposit offers boolean isBuyerMaker = offerUtil.isBuyOffer(direction); - if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) { - throw new IllegalArgumentException("Buyer as taker deposit is required for public offers"); + if (buyerAsTakerWithoutDeposit || isPrivateOffer) { + if (isBuyerMaker) throw new IllegalArgumentException("Buyer must be taker for private offers without deposit"); + if (!buyerAsTakerWithoutDeposit) throw new IllegalArgumentException("Must set buyer as taker without deposit for private offers"); + if (!isPrivateOffer) throw new IllegalArgumentException("Must set offer to private for buyer as taker without deposit"); } // verify fixed price xor market price with margin @@ -149,15 +150,16 @@ public class CreateOfferService { // verify price boolean useMarketBasedPriceValue = fixedPrice == null && useMarketBasedPrice && - isMarketPriceAvailable(currencyCode) && + isExternalPriceAvailable(currencyCode) && !PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId()); 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()); + BigInteger maxTradeLimit = offerUtil.getMaxTradeLimitForRelease(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit); + amount = CoinUtil.getRoundedAmount(amount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, Restrictions.getMinTradeAmount(), maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId()); // generate one-time challenge for private offer String challenge = null; @@ -173,16 +175,15 @@ public class CreateOfferService { double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; long amountAsLong = amount != null ? amount.longValueExact() : 0L; long minAmountAsLong = minAmount != null ? minAmount.longValueExact() : 0L; - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode(); - String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode; + String baseCurrencyCode = Res.getBaseCurrencyCode(); + String counterCurrencyCode = currencyCode; String countryCode = PaymentAccountUtil.getCountryCode(paymentAccount); List acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount); String bankId = PaymentAccountUtil.getBankId(paymentAccount); List acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); + long maxTradeLimitAsLong = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit).longValueExact(); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; @@ -204,8 +205,8 @@ public class CreateOfferService { useMarketBasedPriceValue, amountAsLong, minAmountAsLong, - hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT, - hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT, + HavenoUtils.getMakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit), + HavenoUtils.getTakerFeePct(currencyCode, hasBuyerAsTakerWithoutDeposit), HavenoUtils.PENALTY_FEE_PCT, hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers securityDepositPct, @@ -219,7 +220,7 @@ public class CreateOfferService { acceptedBanks, Version.VERSION, xmrWalletService.getHeight(), - maxTradeLimit, + maxTradeLimitAsLong, maxTradePeriod, useAutoClose, useReOpenAfterAutoClose, @@ -239,7 +240,6 @@ public class CreateOfferService { return offer; } - // TODO: add trigger price? public Offer createClonedOffer(Offer sourceOffer, String currencyCode, Price fixedPrice, @@ -336,7 +336,7 @@ public class CreateOfferService { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private boolean isMarketPriceAvailable(String currencyCode) { + private boolean isExternalPriceAvailable(String currencyCode) { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 8df8511b3a..2d6c4549ff 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -39,7 +39,9 @@ import haveno.core.payment.payload.PaymentMethod; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; import haveno.core.trade.HavenoUtils; +import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; +import haveno.core.util.coin.CoinUtil; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; @@ -173,32 +175,27 @@ public class Offer implements NetworkPayload, PersistablePayload { @Nullable public Price getPrice() { - String currencyCode = getCurrencyCode(); + String counterCurrencyCode = getCounterCurrencyCode(); if (!offerPayload.isUseMarketBasedPrice()) { - return Price.valueOf(currencyCode, offerPayload.getPrice()); + return Price.valueOf(counterCurrencyCode, isInverted() ? PriceUtil.invertLongPrice(offerPayload.getPrice(), counterCurrencyCode) : offerPayload.getPrice()); } checkNotNull(priceFeedService, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + MarketPrice marketPrice = priceFeedService.getMarketPrice(counterCurrencyCode); if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { double factor; double marketPriceMargin = offerPayload.getMarketPriceMarginPct(); - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - factor = getDirection() == OfferDirection.SELL ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } else { - factor = getDirection() == OfferDirection.BUY ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } + factor = getDirection() == OfferDirection.BUY ? + 1 - marketPriceMargin : 1 + marketPriceMargin; double marketPriceAsDouble = marketPrice.getPrice(); double targetPriceAsDouble = marketPriceAsDouble * factor; try { - int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? + int precision = CurrencyUtil.isTraditionalCurrency(counterCurrencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); final long roundedToLong = MathUtils.roundDoubleToLong(scaled); - return Price.valueOf(currencyCode, roundedToLong); + return Price.valueOf(counterCurrencyCode, roundedToLong); } catch (Exception e) { log.error("Exception at getPrice / parseToFiat: " + e + "\n" + "That case should never happen."); @@ -224,7 +221,7 @@ public class Offer implements NetworkPayload, PersistablePayload { return; } - Price tradePrice = Price.valueOf(getCurrencyCode(), price); + Price tradePrice = Price.valueOf(getCounterCurrencyCode(), price); Price offerPrice = getPrice(); if (offerPrice == null) throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); @@ -239,7 +236,7 @@ public class Offer implements NetworkPayload, PersistablePayload { double deviation = Math.abs(1 - relation); log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", - getShortId(), getCurrencyCode(), price, offerPrice.getValue(), + getShortId(), getCounterCurrencyCode(), price, offerPrice.getValue(), deviation * 100 + "%"); if (deviation > PRICE_TOLERANCE) { String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + @@ -251,12 +248,13 @@ public class Offer implements NetworkPayload, PersistablePayload { } @Nullable - public Volume getVolumeByAmount(BigInteger amount) { + public Volume getVolumeByAmount(BigInteger amount, BigInteger minAmount, BigInteger maxAmount) { Price price = getPrice(); if (price == null || amount == null) { return null; } - Volume volumeByAmount = price.getVolumeByAmount(amount); + BigInteger adjustedAmount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, getCounterCurrencyCode(), getPaymentMethodId()); + Volume volumeByAmount = price.getVolumeByAmount(adjustedAmount); volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, getPaymentMethod().getId()); return volumeByAmount; @@ -385,12 +383,12 @@ public class Offer implements NetworkPayload, PersistablePayload { @Nullable public Volume getVolume() { - return getVolumeByAmount(getAmount()); + return getVolumeByAmount(getAmount(), getMinAmount(), getAmount()); } @Nullable public Volume getMinVolume() { - return getVolumeByAmount(getMinAmount()); + return getVolumeByAmount(getMinAmount(), getMinAmount(), getAmount()); } public boolean isBuyOffer() { @@ -507,23 +505,18 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.getCountryCode(); } - public String getCurrencyCode() { - if (currencyCode != null) { - return currencyCode; - } - - currencyCode = offerPayload.getBaseCurrencyCode().equals("XMR") ? - offerPayload.getCounterCurrencyCode() : - offerPayload.getBaseCurrencyCode(); - return currencyCode; + public String getBaseCurrencyCode() { + return isInverted() ? offerPayload.getCounterCurrencyCode() : offerPayload.getBaseCurrencyCode(); // legacy offers inverted crypto } public String getCounterCurrencyCode() { - return offerPayload.getCounterCurrencyCode(); + if (currencyCode != null) return currencyCode; + currencyCode = isInverted() ? offerPayload.getBaseCurrencyCode() : offerPayload.getCounterCurrencyCode(); // legacy offers inverted crypto + return currencyCode; } - public String getBaseCurrencyCode() { - return offerPayload.getBaseCurrencyCode(); + public boolean isInverted() { + return !offerPayload.getBaseCurrencyCode().equals("XMR"); } public String getPaymentMethodId() { @@ -584,21 +577,6 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.isUseReOpenAfterAutoClose(); } - public boolean isXmrAutoConf() { - if (!isXmr()) { - return false; - } - if (getExtraDataMap() == null || !getExtraDataMap().containsKey(OfferPayload.XMR_AUTO_CONF)) { - return false; - } - - return getExtraDataMap().get(OfferPayload.XMR_AUTO_CONF).equals(OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE); - } - - public boolean isXmr() { - return getCurrencyCode().equals("XMR"); - } - public boolean isTraditionalOffer() { return CurrencyUtil.isTraditionalCurrency(currencyCode); } diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 16faa81e57..0fde4e5031 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -149,6 +149,20 @@ public class OfferBookService { Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); announceOfferRemoved(offer); + + // check if invalid offers are now valid + synchronized (invalidOffers) { + for (Offer invalidOffer : new ArrayList(invalidOffers)) { + try { + validateOfferPayload(invalidOffer.getOfferPayload()); + removeInvalidOffer(invalidOffer.getId()); + replaceValidOffer(invalidOffer); + announceOfferAdded(invalidOffer); + } catch (Exception e) { + // ignore + } + } + } } }); }, OfferBookService.class.getSimpleName()); @@ -257,7 +271,7 @@ public class OfferBookService { public List getOffersByCurrency(String direction, String currencyCode) { return getOffers().stream() - .filter(o -> o.getOfferPayload().getBaseCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction) + .filter(o -> o.getOfferPayload().getCounterCurrencyCode().equalsIgnoreCase(currencyCode) && o.getDirection().name() == direction) .collect(Collectors.toList()); } @@ -298,20 +312,6 @@ public class OfferBookService { synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); } - - // check if invalid offers are now valid - synchronized (invalidOffers) { - for (Offer invalidOffer : new ArrayList(invalidOffers)) { - try { - validateOfferPayload(invalidOffer.getOfferPayload()); - removeInvalidOffer(invalidOffer.getId()); - replaceValidOffer(invalidOffer); - announceOfferAdded(invalidOffer); - } catch (Exception e) { - // ignore - } - } - } } private boolean hasValidOffer(String offerId) { @@ -404,7 +404,7 @@ public class OfferBookService { } // validate max offers with same key images - if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId()); + if (numOffersWithSharedKeyImages > Restrictions.getMaxOffersWithSharedFunds()) throw new RuntimeException("More than " + Restrictions.getMaxOffersWithSharedFunds() + " offers exist with same same key images as new offerId=" + offerPayload.getId()); } } @@ -445,11 +445,11 @@ public class OfferBookService { // We filter the case that it is a MarketBasedPrice but the price is not available // That should only be possible if the price feed provider is not available final List offerForJsonList = getOffers().stream() - .filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCurrencyCode()) != null) + .filter(offer -> !offer.isUseMarketBasedPrice() || priceFeedService.getMarketPrice(offer.getCounterCurrencyCode()) != null) .map(offer -> { try { return new OfferForJson(offer.getDirection(), - offer.getCurrencyCode(), + offer.getCounterCurrencyCode(), offer.getMinAmount(), offer.getAmount(), offer.getPrice(), diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java index e64a1ee6eb..206dc4921e 100644 --- a/core/src/main/java/haveno/core/offer/OfferFilterService.java +++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java @@ -127,6 +127,9 @@ public class OfferFilterService { if (isMyInsufficientTradeLimit(offer)) { return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; } + if (!hasValidArbitrator(offer)) { + return Result.ARBITRATOR_NOT_VALIDATED; + } if (!hasValidSignature(offer)) { return Result.SIGNATURE_NOT_VALIDATED; } @@ -159,7 +162,7 @@ public class OfferFilterService { } public boolean isCurrencyBanned(Offer offer) { - return filterManager.isCurrencyBanned(offer.getCurrencyCode()); + return filterManager.isCurrencyBanned(offer.getCounterCurrencyCode()); } public boolean isPaymentMethodBanned(Offer offer) { @@ -201,7 +204,7 @@ public class OfferFilterService { accountAgeWitnessService); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) + offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) .orElse(0L); long offerMinAmount = offer.getMinAmount().longValueExact(); log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", @@ -215,27 +218,28 @@ public class OfferFilterService { return result; } - private boolean hasValidSignature(Offer offer) { + private boolean hasValidArbitrator(Offer offer) { + Arbitrator arbitrator = getArbitrator(offer); + return arbitrator != null; + } - // get accepted arbitrator by address + private Arbitrator getArbitrator(Offer offer) { + + // get arbitrator by address Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); + if (arbitrator != null) return arbitrator; - // accepted arbitrator is null if we are the signing arbitrator - if (arbitrator == null && offer.getOfferPayload().getArbitratorSigner() != null) { - Arbitrator thisArbitrator = user.getRegisteredArbitrator(); - if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) { - if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address? - } else { - - // // otherwise log warning that arbitrator is unregistered - // List arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()); - // if (!arbitratorAddresses.isEmpty()) { - // log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses); - // } - } - } + // check if we are the signing arbitrator + Arbitrator thisArbitrator = user.getRegisteredArbitrator(); + if (thisArbitrator != null && thisArbitrator.getNodeAddress().equals(offer.getOfferPayload().getArbitratorSigner())) return thisArbitrator; - if (arbitrator == null) return false; // invalid arbitrator + // cannot get arbitrator + return null; + } + + private boolean hasValidSignature(Offer offer) { + Arbitrator arbitrator = getArbitrator(offer); + if (arbitrator == null) return false; return HavenoUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator); } diff --git a/core/src/main/java/haveno/core/offer/OfferForJson.java b/core/src/main/java/haveno/core/offer/OfferForJson.java index caebcdc3dd..d96e6090d6 100644 --- a/core/src/main/java/haveno/core/offer/OfferForJson.java +++ b/core/src/main/java/haveno/core/offer/OfferForJson.java @@ -100,14 +100,8 @@ public class OfferForJson { private void setDisplayStrings() { try { final Price price = getPrice(); - - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - primaryMarketDirection = direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; - currencyPair = currencyCode + "/" + Res.getBaseCurrencyCode(); - } else { - primaryMarketDirection = direction; - currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode; - } + primaryMarketDirection = direction; + currencyPair = Res.getBaseCurrencyCode() + "/" + currencyCode; if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { priceDisplayString = traditionalFormat.noCode().format(price.getMonetary()).toString(); @@ -116,7 +110,6 @@ public class OfferForJson { primaryMarketMinVolumeDisplayString = traditionalFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketVolumeDisplayString = traditionalFormat.noCode().format(getVolume().getMonetary()).toString(); } else { - // amount and volume is inverted for json priceDisplayString = cryptoFormat.noCode().format(price.getMonetary()).toString(); primaryMarketMinAmountDisplayString = cryptoFormat.noCode().format(getMinVolume().getMonetary()).toString(); primaryMarketAmountDisplayString = cryptoFormat.noCode().format(getVolume().getMonetary()).toString(); diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 2e2644630a..8792672321 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -56,12 +56,13 @@ import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.ReferralIdService; import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; -import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent; -import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositPct; +import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositPct; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.HashMap; @@ -120,13 +121,13 @@ public class OfferUtil { return direction == OfferDirection.BUY; } - public long getMaxTradeLimit(PaymentAccount paymentAccount, + public BigInteger getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { - return paymentAccount != null + return BigInteger.valueOf(paymentAccount != null ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) - : 0; + : 0); } /** @@ -239,12 +240,12 @@ public class OfferUtil { PaymentAccount paymentAccount, String currencyCode) { checkNotNull(p2PService.getAddress(), "Address must not be null"); - checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(), + checkArgument(securityDeposit <= getMaxSecurityDepositPct(), "securityDeposit must not exceed " + - getMaxSecurityDepositAsPercent()); - checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(), + getMaxSecurityDepositPct()); + checkArgument(securityDeposit >= getMinSecurityDepositPct(), "securityDeposit must not be less than " + - getMinSecurityDepositAsPercent() + " but was " + securityDeposit); + getMinSecurityDepositPct() + " but was " + securityDeposit); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), @@ -263,10 +264,27 @@ public class OfferUtil { } public static boolean isTraditionalOffer(Offer offer) { - return offer.getBaseCurrencyCode().equals("XMR"); + return CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode()); } public static boolean isCryptoOffer(Offer offer) { - return offer.getCounterCurrencyCode().equals("XMR"); + return CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode()); + } + + public BigInteger getMaxTradeLimitForRelease(PaymentAccount paymentAccount, + String currencyCode, + OfferDirection direction, + boolean buyerAsTakerWithoutDeposit) { + + // disallow offers which no buyer can take due to trade limits on release + if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, OfferDirection.BUY, buyerAsTakerWithoutDeposit)); + } + + if (paymentAccount != null) { + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)); + } else { + return BigInteger.ZERO; + } } } diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index f493b1b584..f5a69f7528 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -122,16 +122,16 @@ public final class OpenOffer implements Tradable { this(offer, 0, false); } - public OpenOffer(Offer offer, long triggerPrice) { - this(offer, triggerPrice, false); + public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) { + this(offer, triggerPrice, reserveExactAmount, null); } - public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount) { + public OpenOffer(Offer offer, long triggerPrice, boolean reserveExactAmount, String groupId) { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; this.challenge = offer.getChallenge(); - this.groupId = UUID.randomUUID().toString(); + this.groupId = groupId == null ? UUID.randomUUID().toString() : groupId; state = State.PENDING; } @@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable { return state == State.AVAILABLE; } + public boolean isReserved() { + return state == State.RESERVED; + } + public boolean isDeactivated() { return state == State.DEACTIVATED; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index e5bf40b241..22159a8e11 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -78,6 +78,7 @@ import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.JsonUtil; +import haveno.core.util.PriceUtil; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.BtcWalletService; @@ -519,6 +520,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { + // cannot set trigger price for fixed price offers + if (triggerPrice != 0 && offer.getOfferPayload().getPrice() != 0) { + errorMessageHandler.handleErrorMessage("Cannot set trigger price for fixed price offers."); + return; + } + // check source offer and clone limit OpenOffer sourceOffer = null; if (sourceOfferId != null) { @@ -526,15 +533,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // get source offer Optional sourceOfferOptional = getOpenOffer(sourceOfferId); if (!sourceOfferOptional.isPresent()) { - errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId); + errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId + "."); return; } sourceOffer = sourceOfferOptional.get(); // check clone limit int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size(); - if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) { - errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached."); + if (numClones >= Restrictions.getMaxOffersWithSharedFunds()) { + errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.getMaxOffersWithSharedFunds() + " cloned offers with shared funds reached."); return; } } @@ -632,7 +639,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void applyTriggerState(OpenOffer openOffer) { if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; - if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) { + if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer)) { openOffer.deactivate(true); } } @@ -661,7 +668,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ErrorMessageHandler errorMessageHandler) { log.info("Canceling open offer: {}", openOffer.getId()); if (!offersToBeEdited.containsKey(openOffer.getId())) { - if (openOffer.isAvailable()) { + if (isOnOfferBook(openOffer)) { openOffer.setState(OpenOffer.State.CANCELED); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> { @@ -683,6 +690,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + private boolean isOnOfferBook(OpenOffer openOffer) { + return openOffer.isAvailable() || openOffer.isReserved(); + } + public void editOpenOfferStart(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -1083,6 +1094,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe try { ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user); } catch (Exception e) { + openOffer.getOffer().setState(Offer.State.INVALID); errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage()); return; } @@ -1101,17 +1113,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { // validate non-pending state - try { - validateSignedState(openOffer); - resultHandler.handleResult(null); // done processing if non-pending state is valid - return; - } catch (Exception e) { - log.warn(e.getMessage()); + boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first + if (!skipValidation) { + try { + validateSignedState(openOffer); + resultHandler.handleResult(null); // done processing if non-pending state is valid + return; + } catch (Exception e) { + log.warn(e.getMessage()); - // reset arbitrator signature - openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); - openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + // reset arbitrator signature + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + } } } @@ -1168,9 +1183,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); - resultHandler.handleResult(null); - return; } + + resultHandler.handleResult(null); + return; } } catch (Exception e) { if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); @@ -1186,7 +1202,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature"); } else if (arbitrator == null) { - throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator"); + throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unregistered arbitrator"); } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature"); } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) { @@ -1208,17 +1224,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); // check if split output tx is available for offer - if (splitOutputTx.isLocked()) return splitOutputTx; - else { - boolean isAvailable = true; - for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) { - if (output.isSpent() || output.isFrozen()) { - isAvailable = false; - break; + if (splitOutputTx != null) { + if (splitOutputTx.isLocked()) return splitOutputTx; + else { + boolean isAvailable = true; + for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) { + if (output.isSpent() || output.isFrozen()) { + isAvailable = false; + break; + } } + if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; + else log.warn("Split output tx {} is no longer available for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId()); } - if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; - else log.warn("Split output tx is no longer available for offer {}", openOffer.getId()); + } else { + log.warn("Split output tx {} no longer exists for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId()); } } @@ -1244,7 +1264,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { - List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); + List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsFailed(false)); // TODO: not using setIsIncoming(true) because split output txs sent to self have false; fix in monero-java? Set removeTxs = new HashSet(); for (MoneroTxWallet tx : splitOutputTxs) { if (tx.getOutputs() != null) { // outputs not available until first confirmation @@ -1267,6 +1287,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) + .setIsIncoming(true) .setAmount(amount)).size() > 0); return hasExactTransfer; } @@ -1342,7 +1363,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } catch (Exception e) { if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - xmrWalletService.handleWalletError(e, sourceConnection); + xmrWalletService.handleWalletError(e, sourceConnection, i + 1); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1549,8 +1570,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } // verify max length of extra info - if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { - errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length(); + if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.getMaxExtraInfoLength()) { + errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.getMaxExtraInfoLength() + " but got " + offer.getOfferPayload().getExtraInfo().length(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; @@ -1574,21 +1595,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - // verify the max version number - if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) { - errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION; - log.warn(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; - } - // verify maker and taker fees boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; if (hasBuyerAsTakerWithoutDeposit) { // verify maker's trade fee - if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct(); + double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); + if (offer.getMakerFeePct() != makerFeePct) { + errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; @@ -1603,8 +1617,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } // verify maker security deposit - if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) { - errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + if (offer.getSellerSecurityDepositPct() != Restrictions.getMinSecurityDepositPct()) { + errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; @@ -1619,33 +1633,43 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } else { + // verify public offer (remove to generally allow private offers) + if (offer.isPrivateOffer() || offer.getChallengeHash() != null) { + errorMessage = "Private offer " + request.offerId + " is not valid. It must have direction SELL, taker fee of 0, and a challenge hash."; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + // verify maker's trade fee - if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct(); + double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); + if (offer.getMakerFeePct() != makerFeePct) { + errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + makerFeePct + " but got " + offer.getMakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify taker's trade fee - if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) { - errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct(); + double takerFeePct = HavenoUtils.getTakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); + if (offer.getTakerFeePct() != takerFeePct) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + takerFeePct + " but got " + offer.getTakerFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify seller's security deposit - if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { - errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct(); + if (offer.getSellerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) { + errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getSellerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify buyer's security deposit - if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) { - errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct(); + if (offer.getBuyerSecurityDepositPct() < Restrictions.getMinSecurityDepositPct()) { + errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.getMinSecurityDepositPct() + " but got " + offer.getBuyerSecurityDepositPct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; @@ -1662,17 +1686,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify penalty fee if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { - errorMessage = "Wrong penalty fee for offer " + request.offerId; + errorMessage = "Wrong penalty fee percent for offer " + request.offerId + ". Expected " + HavenoUtils.PENALTY_FEE_PCT + " but got " + offer.getPenaltyFeePct(); log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) - BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT); - BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); + double makerFeePct = HavenoUtils.getMakerFeePct(request.getOfferPayload().getCounterCurrencyCode(), hasBuyerAsTakerWithoutDeposit); + BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), makerFeePct); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); + BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, HavenoUtils.PENALTY_FEE_PCT); MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( offer.getId(), penaltyFee, @@ -1696,7 +1721,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe signedOfferPayload.getPubKeyRing().hashCode(), // trader id signedOfferPayload.getId(), offer.getAmount().longValueExact(), - maxTradeFee.longValueExact(), + penaltyFee.longValueExact(), request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKeyImages(), @@ -1736,6 +1761,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe errorMessage = "Exception at handleSignOfferRequest " + e.getMessage(); log.error(errorMessage + "\n", e); } finally { + if (result == false && errorMessage == null) { + log.warn("Arbitrator is NACKing SignOfferRequest for unknown reason with offerId={}. That should never happen", request.getOfferId()); + log.warn("Printing stacktrace:"); + Thread.dumpStack(); + } sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } } @@ -1925,8 +1955,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe result, errorMessage); - log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}", - reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid()); + if (ackMessage.isSuccess()) { + log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}", + reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid()); + } else { + log.warn("Sending NACK for {} to peer {} with offerId {} and sourceUid {}, errorMessage={}", + reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid(), errorMessage); + } + p2PService.sendEncryptedDirectMessage( sender, senderPubKeyRing, @@ -1952,8 +1988,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// private void maybeUpdatePersistedOffers() { - List openOffersClone = getOpenOffers(); - openOffersClone.forEach(originalOpenOffer -> { + + // update open offers + List updatedOpenOffers = new ArrayList<>(); + getOpenOffers().forEach(originalOpenOffer -> { Offer originalOffer = originalOpenOffer.getOffer(); OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); @@ -2000,30 +2038,31 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId()); } + long normalizedPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOfferPayload.getPrice(), originalOffer.getCounterCurrencyCode()) : originalOfferPayload.getPrice(); OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), originalOfferPayload.getDate(), ownerNodeAddress, originalOfferPayload.getPubKeyRing(), originalOfferPayload.getDirection(), - originalOfferPayload.getPrice(), + normalizedPrice, originalOfferPayload.getMarketPriceMarginPct(), originalOfferPayload.isUseMarketBasedPrice(), originalOfferPayload.getAmount(), originalOfferPayload.getMinAmount(), originalOfferPayload.getMakerFeePct(), originalOfferPayload.getTakerFeePct(), - originalOfferPayload.getPenaltyFeePct(), + HavenoUtils.PENALTY_FEE_PCT, originalOfferPayload.getBuyerSecurityDepositPct(), originalOfferPayload.getSellerSecurityDepositPct(), - originalOfferPayload.getBaseCurrencyCode(), - originalOfferPayload.getCounterCurrencyCode(), + originalOffer.getBaseCurrencyCode(), + originalOffer.getCounterCurrencyCode(), originalOfferPayload.getPaymentMethodId(), originalOfferPayload.getMakerPaymentAccountId(), originalOfferPayload.getCountryCode(), originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getBankId(), originalOfferPayload.getAcceptedBankIds(), - originalOfferPayload.getVersionNr(), + Version.VERSION, originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradePeriod(), @@ -2047,14 +2086,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // create new offer Offer updatedOffer = new Offer(updatedPayload); updatedOffer.setPriceFeedService(priceFeedService); - - OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); - addOpenOffer(updatedOpenOffer); - requestPersistence(); - - log.info("Updating offer completed. id={}", originalOffer.getId()); + long normalizedTriggerPrice = originalOffer.isInverted() ? PriceUtil.invertLongPrice(originalOpenOffer.getTriggerPrice(), originalOffer.getCounterCurrencyCode()) : originalOpenOffer.getTriggerPrice(); + OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, normalizedTriggerPrice, originalOpenOffer.isReserveExactAmount(), originalOpenOffer.getGroupId()); + updatedOpenOffer.setChallenge(originalOpenOffer.getChallenge()); + updatedOpenOffers.add(updatedOpenOffer); } }); + + // add updated open offers + updatedOpenOffers.forEach(updatedOpenOffer -> { + addOpenOffer(updatedOpenOffer); + requestPersistence(); + log.info("Updating offer completed. id={}", updatedOpenOffer.getId()); + }); } @@ -2183,6 +2227,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { + log.info("Refreshing my open offers"); synchronized (openOffers.getList()) { int size = openOffers.size(); //we clone our list as openOffers might change during our delayed call diff --git a/core/src/main/java/haveno/core/offer/TriggerPriceService.java b/core/src/main/java/haveno/core/offer/TriggerPriceService.java index 18527b774c..2b8db3d520 100644 --- a/core/src/main/java/haveno/core/offer/TriggerPriceService.java +++ b/core/src/main/java/haveno/core/offer/TriggerPriceService.java @@ -102,7 +102,7 @@ public class TriggerPriceService { return false; } - String currencyCode = openOffer.getOffer().getCurrencyCode(); + String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); boolean traditionalCurrency = CurrencyUtil.isTraditionalCurrency(currencyCode); int smallestUnitExponent = traditionalCurrency ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : @@ -116,15 +116,13 @@ public class TriggerPriceService { OfferDirection direction = openOffer.getOffer().getDirection(); boolean isSellOffer = direction == OfferDirection.SELL; - boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; - return condition ? + return isSellOffer ? marketPriceAsLong < triggerPrice : marketPriceAsLong > triggerPrice; } private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { - String currencyCode = openOffer.getOffer().getCurrencyCode(); + String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; @@ -162,11 +160,11 @@ public class TriggerPriceService { private void onAddedOpenOffers(List openOffers) { openOffers.forEach(openOffer -> { - String currencyCode = openOffer.getOffer().getCurrencyCode(); + String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>()); openOffersByCurrency.get(currencyCode).add(openOffer); - MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()); + MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()); if (marketPrice != null) { checkPriceThreshold(marketPrice, openOffer); } @@ -175,7 +173,7 @@ public class TriggerPriceService { private void onRemovedOpenOffers(List openOffers) { openOffers.forEach(openOffer -> { - String currencyCode = openOffer.getOffer().getCurrencyCode(); + String currencyCode = openOffer.getOffer().getCounterCurrencyCode(); if (openOffersByCurrency.containsKey(currencyCode)) { Set set = openOffersByCurrency.get(currencyCode); set.remove(openOffer); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 60eaa64cdc..c0dd076232 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -72,10 +72,10 @@ public class MakerReserveOfferFunds extends Task { model.getProtocol().startTimeoutTimer(); // collect relevant info - BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct()); BigInteger makerFee = offer.getMaxMakerFee(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); + BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); @@ -100,7 +100,7 @@ public class MakerReserveOfferFunds extends Task { throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - model.getXmrWalletService().handleWalletError(e, sourceConnection); + model.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); verifyPending(); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index 3644492735..3559750830 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory; import java.util.Date; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -60,8 +61,16 @@ public class MakerSendSignOfferRequest extends Task { try { runInterceptHook(); - // create request for arbitrator to sign offer - String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); + // get payout address entry + String returnAddress; + Optional addressEntryOpt = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT); + if (addressEntryOpt.isPresent()) returnAddress = addressEntryOpt.get().getAddressString(); + else { + log.warn("Payout address entry found for unsigned offer {} is missing, creating anew", offer.getId()); + returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + } + + // build sign offer request SignOfferRequest request = new SignOfferRequest( offer.getId(), P2PService.getMyNodeAddress(), diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java index 3b1a01beeb..518ac787ad 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java @@ -23,6 +23,7 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; +import haveno.core.payment.PaymentAccount; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; import haveno.core.user.User; @@ -96,7 +97,10 @@ public class ValidateOffer extends Task { /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ - long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); + PaymentAccount paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + checkArgument(paymentAccount != null, "Payment account is null. makerPaymentAccountId=" + offer.getMakerPaymentAccountId()); + + long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCounterCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); checkArgument(offer.getAmount().longValueExact() <= maxAmount, "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); @@ -108,7 +112,7 @@ public class ValidateOffer extends Task { checkArgument(offer.getDate().getTime() > 0, "Date must not be 0. date=" + offer.getDate().toString()); - checkNotNull(offer.getCurrencyCode(), "Currency is null"); + checkNotNull(offer.getCounterCurrencyCode(), "Currency is null"); checkNotNull(offer.getDirection(), "Direction is null"); checkNotNull(offer.getId(), "Id is null"); checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null"); diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java index 49c6da12e9..3ff3da0b68 100644 --- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java @@ -106,7 +106,7 @@ public class TakeOfferModel implements Model { calculateTotalToPay(); offer.resetState(); - priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } @Override @@ -147,7 +147,7 @@ public class TakeOfferModel implements Model { private long getMaxTradeLimit() { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), + offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); } diff --git a/core/src/main/java/haveno/core/payment/AchTransferAccount.java b/core/src/main/java/haveno/core/payment/AchTransferAccount.java index 9f04e4e076..8c9c1eee62 100644 --- a/core/src/main/java/haveno/core/payment/AchTransferAccount.java +++ b/core/src/main/java/haveno/core/payment/AchTransferAccount.java @@ -19,6 +19,7 @@ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.BankUtil; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.AchTransferAccountPayload; import haveno.core.payment.payload.BankAccountPayload; @@ -34,6 +35,19 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.HOLDER_ADDRESS, + PaymentAccountFormField.FieldId.BANK_NAME, + PaymentAccountFormField.FieldId.BRANCH_ID, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.ACCOUNT_TYPE, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public AchTransferAccount() { super(PaymentMethod.ACH_TRANSFER); } @@ -79,6 +93,15 @@ public final class AchTransferAccount extends CountryBasedPaymentAccount impleme @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + if (field.getId() == PaymentAccountFormField.FieldId.BRANCH_ID) field.setLabel(BankUtil.getBranchIdLabel("US")); + if (field.getId() == PaymentAccountFormField.FieldId.ACCOUNT_TYPE) field.setLabel(BankUtil.getAccountTypeLabel("US")); + return field; } } diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java index 1bff92b5cd..af2f617313 100644 --- a/core/src/main/java/haveno/core/payment/AliPayAccount.java +++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java @@ -60,6 +60,13 @@ public final class AliPayAccount extends PaymentAccount { new TraditionalCurrency("ZAR") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + public AliPayAccount() { super(PaymentMethod.ALI_PAY); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -77,7 +84,7 @@ public final class AliPayAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { diff --git a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java index cb3eb35c70..65cf5719ae 100644 --- a/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java +++ b/core/src/main/java/haveno/core/payment/AmazonGiftCardAccount.java @@ -46,6 +46,14 @@ public final class AmazonGiftCardAccount extends PaymentAccount { new TraditionalCurrency("USD") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + @Nullable private Country country; @@ -65,7 +73,7 @@ public final class AmazonGiftCardAccount extends PaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public String getEmailOrMobileNr() { @@ -97,4 +105,11 @@ public final class AmazonGiftCardAccount extends PaymentAccount { private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() { return (AmazonGiftCardAccountPayload) paymentAccountPayload; } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + return field; + } } diff --git a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java index 579167c276..456124aac6 100644 --- a/core/src/main/java/haveno/core/payment/InteracETransferAccount.java +++ b/core/src/main/java/haveno/core/payment/InteracETransferAccount.java @@ -17,12 +17,15 @@ package haveno.core.payment; +import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.InteracETransferAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.validation.InteracETransferValidator; +import haveno.core.trade.HavenoUtils; import lombok.EqualsAndHashCode; import org.jetbrains.annotations.NotNull; @@ -33,6 +36,15 @@ public final class InteracETransferAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CAD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR, + PaymentAccountFormField.FieldId.QUESTION, + PaymentAccountFormField.FieldId.ANSWER, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public InteracETransferAccount() { super(PaymentMethod.INTERAC_E_TRANSFER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -50,15 +62,15 @@ public final class InteracETransferAccount extends PaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setEmail(String email) { - ((InteracETransferAccountPayload) paymentAccountPayload).setEmail(email); + ((InteracETransferAccountPayload) paymentAccountPayload).setEmailOrMobileNr(email); } public String getEmail() { - return ((InteracETransferAccountPayload) paymentAccountPayload).getEmail(); + return ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr(); } public void setAnswer(String answer) { @@ -84,4 +96,19 @@ public final class InteracETransferAccount extends PaymentAccount { public String getHolderName() { return ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName(); } + + public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { + InteracETransferValidator interacETransferValidator = HavenoUtils.corePaymentAccountService.interacETransferValidator; + switch (fieldId) { + case QUESTION: + processValidationResult(interacETransferValidator.questionValidator.validate(value)); + break; + case ANSWER: + processValidationResult(interacETransferValidator.answerValidator.validate(value)); + break; + default: + super.validateFormField(form, fieldId, value); + } + + } } diff --git a/core/src/main/java/haveno/core/payment/JapanBankData.java b/core/src/main/java/haveno/core/payment/JapanBankData.java index 9181e0f48a..c8919acec0 100644 --- a/core/src/main/java/haveno/core/payment/JapanBankData.java +++ b/core/src/main/java/haveno/core/payment/JapanBankData.java @@ -18,8 +18,7 @@ package haveno.core.payment; import com.google.common.collect.ImmutableMap; -import com.google.inject.Inject; -import haveno.core.user.Preferences; +import haveno.core.trade.HavenoUtils; import java.util.ArrayList; import java.util.List; @@ -47,13 +46,6 @@ import java.util.Map; public class JapanBankData { - private static String userLanguage; - - @Inject - JapanBankData(Preferences preferences) { - userLanguage = preferences.getUserLanguage(); - } - /* Returns the main list of ~500 banks in Japan with bank codes, but since 90%+ of people will be using one of ~30 major banks, @@ -793,7 +785,7 @@ public class JapanBankData { // don't localize these strings into all languages, // all we want is either Japanese or English here. public static String getString(String id) { - boolean ja = userLanguage.equals("ja"); + boolean ja = HavenoUtils.preferences.getUserLanguage().equals("ja"); switch (id) { case "bank": diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 14dd88482b..40351b1ca6 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -348,7 +348,7 @@ public abstract class PaymentAccount implements PersistablePayload { if (paymentAccountPayload != null) { String payloadJson = paymentAccountPayload.toJson(); Map payloadMap = gson.fromJson(payloadJson, new TypeToken>() {}.getType()); - + for (Map.Entry entry : payloadMap.entrySet()) { Object value = entry.getValue(); if (value instanceof List) { @@ -360,7 +360,7 @@ public abstract class PaymentAccount implements PersistablePayload { jsonMap.putAll(payloadMap); } - + jsonMap.put("accountName", getAccountName()); jsonMap.put("accountId", getId()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); @@ -435,7 +435,8 @@ public abstract class PaymentAccount implements PersistablePayload { processValidationResult(new LengthValidator(2, 100).validate(value)); break; case ACCOUNT_TYPE: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(2, 100).validate(value)); + break; case ANSWER: throw new IllegalArgumentException("Not implemented"); case BANK_ACCOUNT_NAME: @@ -491,7 +492,8 @@ public abstract class PaymentAccount implements PersistablePayload { processValidationResult(new BICValidator().validate(value)); break; case BRANCH_ID: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(2, 34).validate(value)); + break; case CITY: processValidationResult(new LengthValidator(2, 34).validate(value)); break; @@ -518,7 +520,8 @@ public abstract class PaymentAccount implements PersistablePayload { case EXTRA_INFO: break; case HOLDER_ADDRESS: - throw new IllegalArgumentException("Not implemented"); + processValidationResult(new LengthValidator(0, 100).validate(value)); + break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: @@ -616,16 +619,20 @@ public abstract class PaymentAccount implements PersistablePayload { break; case ACCOUNT_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); - field.setLabel("payment.accountNr"); + field.setLabel(Res.get("payment.accountNr")); break; case ACCOUNT_OWNER: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner")); break; case ACCOUNT_TYPE: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); + field.setLabel(Res.get("payment.select.account")); + break; case ANSWER: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.answer")); + break; case BANK_ACCOUNT_NAME: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.owner")); @@ -668,11 +675,11 @@ public abstract class PaymentAccount implements PersistablePayload { break; case BENEFICIARY_ACCOUNT_NR: field.setComponent(PaymentAccountFormField.Component.TEXT); - field.setLabel(Res.get("payment.swift.account")); + field.setLabel(Res.get("payment.swift.account")); // TODO: this is specific to swift break; case BENEFICIARY_ADDRESS: field.setComponent(PaymentAccountFormField.Component.TEXTAREA); - field.setLabel(Res.get("payment.swift.address.beneficiary")); + field.setLabel(Res.get("payment.swift.address.beneficiary")); // TODO: this is specific to swift break; case BENEFICIARY_CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); @@ -691,7 +698,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel("BIC"); break; case BRANCH_ID: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + //field.setLabel("Not implemented"); // expected to be overridden by subclasses + break; case CITY: field.setComponent(PaymentAccountFormField.Component.TEXT); field.setLabel(Res.get("payment.account.city")); @@ -717,7 +726,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.shared.optionalExtra")); break; case HOLDER_ADDRESS: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXTAREA); + field.setLabel(Res.get("payment.account.owner.address")); + break; case HOLDER_EMAIL: throw new IllegalArgumentException("Not implemented"); case HOLDER_NAME: @@ -755,7 +766,9 @@ public abstract class PaymentAccount implements PersistablePayload { field.setLabel(Res.get("payment.swift.swiftCode.intermediary")); break; case MOBILE_NR: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.mobile")); + break; case NATIONAL_ACCOUNT_ID: throw new IllegalArgumentException("Not implemented"); case PAYID: @@ -771,7 +784,9 @@ public abstract class PaymentAccount implements PersistablePayload { case PROMPT_PAY_ID: throw new IllegalArgumentException("Not implemented"); case QUESTION: - throw new IllegalArgumentException("Not implemented"); + field.setComponent(PaymentAccountFormField.Component.TEXT); + field.setLabel(Res.get("payment.secret")); + break; case REQUIREMENTS: throw new IllegalArgumentException("Not implemented"); case SALT: diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java index 0226fe4530..6ca6ea2bed 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java @@ -50,6 +50,7 @@ import static haveno.common.util.Utilities.decodeFromHex; import static haveno.core.locale.CountryUtil.findCountryByCode; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; +import static haveno.core.payment.payload.PaymentMethod.AMAZON_GIFT_CARD_ID; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; import static java.lang.String.format; import static java.util.Arrays.stream; @@ -438,6 +439,8 @@ class PaymentAccountTypeAdapter extends TypeAdapter { // account.setSingleTradeCurrency(fiatCurrency); } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); + } else if (account.hasPaymentMethodWithId(AMAZON_GIFT_CARD_ID)) { + ((AmazonGiftCardAccount) account).setCountry(country.get()); } else { String errMsg = format("cannot set the country on a %s", paymentAccountType.getSimpleName()); diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java index 11575aeb62..0ce1038769 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java @@ -122,9 +122,9 @@ public class PaymentAccountUtil { public static boolean isAmountValidForOffer(Offer offer, PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService) { - boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); + boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); + offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); return !hasChargebackRisk || hasValidAccountAgeWitness; } diff --git a/core/src/main/java/haveno/core/payment/ReceiptPredicates.java b/core/src/main/java/haveno/core/payment/ReceiptPredicates.java index a56fa4491b..33cb5d0dd6 100644 --- a/core/src/main/java/haveno/core/payment/ReceiptPredicates.java +++ b/core/src/main/java/haveno/core/payment/ReceiptPredicates.java @@ -95,7 +95,7 @@ class ReceiptPredicates { .map(TradeCurrency::getCode) .collect(Collectors.toSet()); - return codes.contains(offer.getCurrencyCode()); + return codes.contains(offer.getCounterCurrencyCode()); } boolean isMatchingSepaOffer(Offer offer, PaymentAccount account) { diff --git a/core/src/main/java/haveno/core/payment/SwishAccount.java b/core/src/main/java/haveno/core/payment/SwishAccount.java index a726a9a14a..eb2e10de87 100644 --- a/core/src/main/java/haveno/core/payment/SwishAccount.java +++ b/core/src/main/java/haveno/core/payment/SwishAccount.java @@ -17,12 +17,14 @@ package haveno.core.payment; +import haveno.core.api.model.PaymentAccountForm; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.payload.SwishAccountPayload; +import haveno.core.payment.validation.SwishValidator; import lombok.EqualsAndHashCode; import lombok.NonNull; @@ -33,6 +35,13 @@ public final class SwishAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("SEK")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.MOBILE_NR, + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public SwishAccount() { super(PaymentMethod.SWISH); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -50,7 +59,7 @@ public final class SwishAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setMobileNr(String mobileNr) { @@ -68,4 +77,16 @@ public final class SwishAccount extends PaymentAccount { public String getHolderName() { return ((SwishAccountPayload) paymentAccountPayload).getHolderName(); } + + @Override + public void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { + switch (fieldId) { + case MOBILE_NR: + processValidationResult(new SwishValidator().validate(value)); + break; + default: + super.validateFormField(form, fieldId, value); + break; + } + } } diff --git a/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java b/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java index 94491ddbf0..2702ffbaca 100644 --- a/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java +++ b/core/src/main/java/haveno/core/payment/TransferwiseUsdAccount.java @@ -19,6 +19,7 @@ package haveno.core.payment; import haveno.core.api.model.PaymentAccountFormField; import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; @@ -33,6 +34,15 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.EMAIL, + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.HOLDER_ADDRESS, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.COUNTRY, + PaymentAccountFormField.FieldId.SALT + ); + public TransferwiseUsdAccount() { super(PaymentMethod.TRANSFERWISE_USD); // this payment method is currently restricted to United States/USD @@ -61,11 +71,11 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { } public void setBeneficiaryAddress(String address) { - ((TransferwiseUsdAccountPayload) paymentAccountPayload).setBeneficiaryAddress(address); + ((TransferwiseUsdAccountPayload) paymentAccountPayload).setHolderAddress(address); } public String getBeneficiaryAddress() { - return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + return ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); } @Override @@ -90,6 +100,13 @@ public final class TransferwiseUsdAccount extends CountryBasedPaymentAccount { @Override public @NotNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.HOLDER_ADDRESS) field.setLabel(field.getLabel() + " " + Res.get("payment.transferwiseUsd.address")); + return field; } } diff --git a/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java b/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java index 41667563cf..4cf6e20608 100644 --- a/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java +++ b/core/src/main/java/haveno/core/payment/USPostalMoneyOrderAccount.java @@ -33,6 +33,13 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount { public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("USD")); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.HOLDER_NAME, + PaymentAccountFormField.FieldId.POSTAL_ADDRESS, + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.SALT + ); + public USPostalMoneyOrderAccount() { super(PaymentMethod.US_POSTAL_MONEY_ORDER); setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); @@ -50,7 +57,7 @@ public final class USPostalMoneyOrderAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setPostalAddress(String postalAddress) { diff --git a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java index 297968ef0c..9071aa7aea 100644 --- a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java +++ b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java @@ -38,6 +38,13 @@ public final class WeChatPayAccount extends PaymentAccount { new TraditionalCurrency("GBP") ); + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.ACCOUNT_NR, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); } @@ -54,7 +61,7 @@ public final class WeChatPayAccount extends PaymentAccount { @Override public @NonNull List getInputFieldIds() { - throw new RuntimeException("Not implemented"); + return INPUT_FIELD_IDS; } public void setAccountNr(String accountNr) { diff --git a/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java index 26105e7478..751ecbf045 100644 --- a/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/InteracETransferAccountPayload.java @@ -36,7 +36,7 @@ import java.util.Map; @Getter @Slf4j public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { - private String email = ""; + private String emailOrMobileNr = ""; private String holderName = ""; private String question = ""; private String answer = ""; @@ -52,7 +52,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload private InteracETransferAccountPayload(String paymentMethod, String id, - String email, + String emailOrMobileNr, String holderName, String question, String answer, @@ -62,7 +62,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload id, maxTradePeriod, excludeFromJsonDataMap); - this.email = email; + this.emailOrMobileNr = emailOrMobileNr; this.holderName = holderName; this.question = question; this.answer = answer; @@ -72,7 +72,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setInteracETransferAccountPayload(protobuf.InteracETransferAccountPayload.newBuilder() - .setEmail(email) + .setEmailOrMobileNr(emailOrMobileNr) .setHolderName(holderName) .setQuestion(question) .setAnswer(answer)) @@ -82,7 +82,7 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload public static InteracETransferAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return new InteracETransferAccountPayload(proto.getPaymentMethodId(), proto.getId(), - proto.getInteracETransferAccountPayload().getEmail(), + proto.getInteracETransferAccountPayload().getEmailOrMobileNr(), proto.getInteracETransferAccountPayload().getHolderName(), proto.getInteracETransferAccountPayload().getQuestion(), proto.getInteracETransferAccountPayload().getAnswer(), @@ -98,21 +98,21 @@ public final class InteracETransferAccountPayload extends PaymentAccountPayload @Override public String getPaymentDetails() { return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.owner") + " " + holderName + ", " + - Res.get("payment.email") + " " + email + ", " + Res.getWithCol("payment.secret") + " " + + Res.get("payment.email") + " " + emailOrMobileNr + ", " + Res.getWithCol("payment.secret") + " " + question + ", " + Res.getWithCol("payment.answer") + " " + answer; } @Override public String getPaymentDetailsForTradePopup() { return Res.getWithCol("payment.account.owner") + " " + holderName + "\n" + - Res.getWithCol("payment.email") + " " + email + "\n" + + Res.getWithCol("payment.email") + " " + emailOrMobileNr + "\n" + Res.getWithCol("payment.secret") + " " + question + "\n" + Res.getWithCol("payment.answer") + " " + answer; } @Override public byte[] getAgeWitnessInputData() { - return super.getAgeWitnessInputData(ArrayUtils.addAll(email.getBytes(StandardCharsets.UTF_8), + return super.getAgeWitnessInputData(ArrayUtils.addAll(emailOrMobileNr.getBytes(StandardCharsets.UTF_8), ArrayUtils.addAll(question.getBytes(StandardCharsets.UTF_8), answer.getBytes(StandardCharsets.UTF_8)))); } diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 4a8246fe78..dd842af7c3 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -369,7 +369,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } diff --git a/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java index 1daba610db..bdc3243a30 100644 --- a/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/TransferwiseUsdAccountPayload.java @@ -39,7 +39,7 @@ import java.util.Map; public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAccountPayload { private String email = ""; private String holderName = ""; - private String beneficiaryAddress = ""; + private String holderAddress = ""; public TransferwiseUsdAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -51,7 +51,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco List acceptedCountryCodes, String email, String holderName, - String beneficiaryAddress, + String holderAddress, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -63,7 +63,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco this.email = email; this.holderName = holderName; - this.beneficiaryAddress = beneficiaryAddress; + this.holderAddress = holderAddress; } @Override @@ -71,7 +71,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco protobuf.TransferwiseUsdAccountPayload.Builder builder = protobuf.TransferwiseUsdAccountPayload.newBuilder() .setEmail(email) .setHolderName(holderName) - .setBeneficiaryAddress(beneficiaryAddress); + .setHolderAddress(holderAddress); final protobuf.CountryBasedPaymentAccountPayload.Builder countryBasedPaymentAccountPayload = getPaymentAccountPayloadBuilder() .getCountryBasedPaymentAccountPayloadBuilder() .setTransferwiseUsdAccountPayload(builder); @@ -89,7 +89,7 @@ public final class TransferwiseUsdAccountPayload extends CountryBasedPaymentAcco new ArrayList<>(countryBasedPaymentAccountPayload.getAcceptedCountryCodesList()), accountPayloadPB.getEmail(), accountPayloadPB.getHolderName(), - accountPayloadPB.getBeneficiaryAddress(), + accountPayloadPB.getHolderAddress(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } diff --git a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java index 4545a4e210..f2c047f5c8 100644 --- a/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java +++ b/core/src/main/java/haveno/core/payment/validation/SecurityDepositValidator.java @@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator { private ValidationResult validateIfNotTooLowPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double minPercentage = Restrictions.getMinSecurityDepositAsPercent(); + double minPercentage = Restrictions.getMinSecurityDepositPct(); if (percentage < minPercentage) return new ValidationResult(false, Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage))); @@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator { private ValidationResult validateIfNotTooHighPercentageValue(String input) { try { double percentage = ParsingUtils.parsePercentStringToDouble(input); - double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent(); + double maxPercentage = Restrictions.getMaxSecurityDepositPct(); if (percentage > maxPercentage) return new ValidationResult(false, Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); diff --git a/core/src/main/java/haveno/core/provider/ProvidersRepository.java b/core/src/main/java/haveno/core/provider/ProvidersRepository.java index 08736e0f76..508252b491 100644 --- a/core/src/main/java/haveno/core/provider/ProvidersRepository.java +++ b/core/src/main/java/haveno/core/provider/ProvidersRepository.java @@ -53,7 +53,7 @@ public class ProvidersRepository { private static final List DEFAULT_NODES = Arrays.asList( "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake - "http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck + "http://agorise7ae5g7lkqp7r7qddsyzskft7cqhgguwkadbqamtsrap5onead.onion/" // Agorise ); private final Config config; diff --git a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java index c58277792f..ef4ab94e43 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java @@ -296,13 +296,13 @@ public class PriceFeedService { } } - private void setHavenoMarketPrice(String currencyCode, Price price) { + private void setHavenoMarketPrice(String counterCurrencyCode, Price price) { UserThread.execute(() -> { - String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode); + String counterCurrencyCodeBase = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode); synchronized (cache) { - if (!cache.containsKey(currencyCodeBase) || !cache.get(currencyCodeBase).isExternallyProvidedPrice()) { - cache.put(currencyCodeBase, new MarketPrice(currencyCodeBase, - MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT), + if (!cache.containsKey(counterCurrencyCodeBase) || !cache.get(counterCurrencyCodeBase).isExternallyProvidedPrice()) { + cache.put(counterCurrencyCodeBase, new MarketPrice(counterCurrencyCodeBase, + MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(counterCurrencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT), 0, false)); } @@ -371,9 +371,7 @@ public class PriceFeedService { } /** - * Returns prices for all available currencies. - * For crypto currencies the value is XMR price for 1 unit of given crypto currency (e.g. 1 DOGE = X XMR). - * For traditional currencies the value is price in the given traditional currency per 1 XMR (e.g. 1 XMR = X USD). + * Returns prices for all available currencies. The base currency is always XMR. * * TODO: instrument requestPrices() result and fault handlers instead of using CountDownLatch and timeout */ diff --git a/core/src/main/java/haveno/core/provider/price/PriceProvider.java b/core/src/main/java/haveno/core/provider/price/PriceProvider.java index 931629473a..d8a7f2bef7 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceProvider.java +++ b/core/src/main/java/haveno/core/provider/price/PriceProvider.java @@ -28,6 +28,8 @@ import haveno.network.p2p.P2PService; import lombok.extern.slf4j.Slf4j; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -63,16 +65,20 @@ public class PriceProvider extends HttpClientProvider { LinkedTreeMap treeMap = (LinkedTreeMap) obj; String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode"); String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode"); - String currencyCode = baseCurrencyCode.equals("XMR") ? counterCurrencyCode : baseCurrencyCode; - currencyCode = CurrencyUtil.getCurrencyCodeBase(currencyCode); + boolean isInverted = !"XMR".equalsIgnoreCase(baseCurrencyCode); + if (isInverted) { + String temp = baseCurrencyCode; + baseCurrencyCode = counterCurrencyCode; + counterCurrencyCode = temp; + } + counterCurrencyCode = CurrencyUtil.getCurrencyCodeBase(counterCurrencyCode); double price = (Double) treeMap.get("price"); - // json uses double for our timestampSec long value... + if (isInverted) price = BigDecimal.ONE.divide(BigDecimal.valueOf(price), 10, RoundingMode.HALF_UP).doubleValue(); // XMR is always base currency, so invert price if applicable long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); - marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + marketPriceMap.put(counterCurrencyCode, new MarketPrice(counterCurrencyCode, price, timestampSec, true)); } catch (Throwable t) { log.error("Error getting all prices: {}\n", t.getMessage(), t); } - }); return marketPriceMap; } diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index 4bb8e86d82..8764794e12 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -154,15 +154,15 @@ public abstract class SupportManager { // Message handler /////////////////////////////////////////////////////////////////////////////////////////// - protected void handleChatMessage(ChatMessage chatMessage) { + protected void handle(ChatMessage chatMessage) { final String tradeId = chatMessage.getTradeId(); final String uid = chatMessage.getUid(); log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid); boolean channelOpen = channelOpen(chatMessage); if (!channelOpen) { - log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); + log.warn("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { - Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1); + Timer timer = UserThread.runAfter(() -> handle(chatMessage), 1); delayMsgMap.put(uid, timer); } else { String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; @@ -217,7 +217,11 @@ public abstract class SupportManager { synchronized (dispute.getChatMessages()) { for (ChatMessage chatMessage : dispute.getChatMessages()) { if (chatMessage.getUid().equals(ackMessage.getSourceUid())) { - if (trade.getDisputeState().isCloseRequested()) { + if (trade.getDisputeState().isRequested()) { + log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress()); + dispute.setIsClosed(); + trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); + } else if (trade.getDisputeState().isCloseRequested()) { log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress()); dispute.setIsClosed(); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED); diff --git a/core/src/main/java/haveno/core/support/dispute/Dispute.java b/core/src/main/java/haveno/core/support/dispute/Dispute.java index 15595b8893..01f0493aa3 100644 --- a/core/src/main/java/haveno/core/support/dispute/Dispute.java +++ b/core/src/main/java/haveno/core/support/dispute/Dispute.java @@ -392,7 +392,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { change += "chat messages;"; } if (change.length() > 0) { - log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); + log.info("Cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId())); } } diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index ac0ede6e35..fd29e1583c 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -64,6 +64,7 @@ import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; +import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradePeer; @@ -222,7 +223,7 @@ public abstract class DisputeManager> extends Sup /////////////////////////////////////////////////////////////////////////////////////////// // We get this message at both peers. The dispute object is in context of the trader - public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage); + public abstract void handle(DisputeClosedMessage disputeClosedMessage); public abstract NodeAddress getAgentNodeAddress(Dispute dispute); @@ -403,6 +404,24 @@ public abstract class DisputeManager> extends Sup chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); + // try to import latest multisig info + try { + trade.importMultisigHex(); + } catch (Exception e) { + log.error("Failed to import multisig hex", e); + } + + // try to export latest multisig info + try { + trade.exportMultisigHex(); + if (trade instanceof SellerTrade) { + trade.getProcessModel().setPaymentSentPayoutTxStale(true); // exporting multisig hex will invalidate previously unsigned payout txs + trade.getSelf().setUnsignedPayoutTxHex(null); + } + } catch (Exception e) { + log.error("Failed to export multisig hex", e); + } + // create dispute opened message NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, @@ -481,18 +500,14 @@ public abstract class DisputeManager> extends Sup } // arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator - protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) { + protected void handle(DisputeOpenedMessage message) { Dispute msgDispute = message.getDispute(); log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId()); // get trade Trade trade = tradeManager.getTrade(msgDispute.getTradeId()); if (trade == null) { - log.warn("Dispute trade {} does not exist", msgDispute.getTradeId()); - return; - } - if (trade.isPayoutPublished()) { - log.warn("Dispute trade {} payout already published", msgDispute.getTradeId()); + log.warn("Ignoring DisputeOpenedMessage for trade {} because it does not exist", msgDispute.getTradeId()); return; } @@ -500,7 +515,7 @@ public abstract class DisputeManager> extends Sup Optional storedDisputeOptional = findDispute(msgDispute); // determine if re-opening dispute - boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed(); + boolean reOpen = storedDisputeOptional.isPresent(); // use existing dispute or create new Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute; @@ -579,6 +594,21 @@ public abstract class DisputeManager> extends Sup TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender; if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex()); + // TODO: peer needs to import multisig hex at some point + // TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too + // TODO: arbitrator needs to import multisig info then scan for updated state? + + // sync and poll wallet unless finalized + if (!trade.isPayoutFinalized()) { + trade.syncAndPollWallet(); + trade.recoverIfMissingWalletData(); + } + + // nack if payout published + if (trade.isPayoutPublished()) { + throw new RuntimeException("Ignoring DisputeOpenedMessage because payout is already published for " + trade.getClass().getSimpleName() + " " + trade.getId() + ", payoutTxId=" + trade.getPayoutTxId()); + } + // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); @@ -925,66 +955,73 @@ public abstract class DisputeManager> extends Sup // sync and poll trade.syncAndPollWallet(); - // create unsigned dispute payout tx if not already published - if (!trade.isPayoutPublished()) { + // recover if missing wallet data + trade.recoverIfMissingWalletData(); - // create unsigned dispute payout tx - if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId()); - try { + // check if payout tx already published + String alreadyPublishedMsg = "Cannot create dispute payout tx because payout tx is already published for trade " + trade.getId(); + if (trade.isPayoutPublished()) throw new RuntimeException(alreadyPublishedMsg); - // trade wallet must be synced - if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId()); + // create unsigned dispute payout tx + if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId()); + try { - // check amounts - if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); - if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); - if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) { - throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); - } + // trade wallet must be synced + if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId()); - // create dispute payout tx config - MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); - String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); - String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); - txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); - if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); - if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); - - // configure who pays mining fee - BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); - if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 - else { - switch (disputeResult.getSubtractFeeFrom()) { - case BUYER_AND_SELLER: - txConfig.setSubtractFeeFrom(0, 1); - break; - case BUYER_ONLY: - txConfig.setSubtractFeeFrom(0); - break; - case SELLER_ONLY: - txConfig.setSubtractFeeFrom(1); - break; - } - } - - // create dispute payout tx - MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig); - - // update trade state - if (updateState) { - trade.getProcessModel().setUnsignedPayoutTx(payoutTx); - trade.updatePayout(payoutTx); - if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - } - trade.requestPersistence(); - return payoutTx; - } catch (Exception e) { - trade.syncAndPollWallet(); - if (!trade.isPayoutPublished()) throw e; + // check amounts + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); + if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) { + throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); } + + // create dispute payout tx config + MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); + String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); + String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); + txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); + + // configure who pays mining fee + BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); + if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 + else { + switch (disputeResult.getSubtractFeeFrom()) { + case BUYER_AND_SELLER: + txConfig.setSubtractFeeFrom(0, 1); + break; + case BUYER_ONLY: + txConfig.setSubtractFeeFrom(0); + break; + case SELLER_ONLY: + txConfig.setSubtractFeeFrom(1); + break; + } + } + + // create dispute payout tx + MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig); + + // update trade state + if (updateState) { + trade.getProcessModel().setUnsignedPayoutTx(payoutTx); + trade.setPayoutTx(payoutTx); + if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } + trade.requestPersistence(); + return payoutTx; + } catch (Exception e) { + trade.syncAndPollWallet(); + if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); + throw e; + } catch (AssertionError e) { // tx creation throws assertion error with invalid config + trade.syncAndPollWallet(); + if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); + throw new RuntimeException(e); } - return null; // can be null if already published or we don't have receiver's multisig hex } private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java index 0905af4a1d..c7dc71cfde 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java @@ -42,10 +42,15 @@ import static com.google.common.base.Preconditions.checkNotNull; public class DisputeValidation { public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException { - if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId()); - if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract"); + if (dispute.getSellerPaymentAccountPayload() != null) { + if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) { + throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract"); + } + } if (dispute.getBuyerPaymentAccountPayload() != null) { - if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract"); + if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) { + throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract"); + } } } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 5ac7cd389a..4e43f7ed79 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -148,11 +148,11 @@ public final class ArbitrationManager extends DisputeManager { if (message instanceof DisputeOpenedMessage) { - handleDisputeOpenedMessage((DisputeOpenedMessage) message); + handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { - handleChatMessage((ChatMessage) message); + handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { - handleDisputeClosedMessage((DisputeClosedMessage) message); + handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } @@ -226,11 +226,11 @@ public final class ArbitrationManager extends DisputeManager XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff); + HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), arbitratorSignedPayoutTx.getFee()); + log.info("Dispute payout tx fee is within tolerance for {} {}", getClass().getSimpleName(), trade.getShortId()); } } else { + log.warn("Payout tx already signed for {} {}, skipping signing", getClass().getSimpleName(), trade.getShortId()); disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex()); } @@ -504,8 +510,8 @@ public final class ArbitrationManager extends DisputeManager message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeOpenedMessage) { - handleDisputeOpenedMessage((DisputeOpenedMessage) message); + handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { - handleChatMessage((ChatMessage) message); + handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { - handleDisputeClosedMessage((DisputeClosedMessage) message); + handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } @@ -150,7 +150,7 @@ public final class MediationManager extends DisputeManager @Override // We get that message at both peers. The dispute object is in context of the trader - public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) { + public void handle(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); @@ -163,7 +163,7 @@ public final class MediationManager extends DisputeManager "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2); + Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + diff --git a/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java b/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java index 88a1b6a9df..e4f6828c19 100644 --- a/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java +++ b/core/src/main/java/haveno/core/support/dispute/messages/DisputeClosedMessage.java @@ -35,6 +35,7 @@ import static com.google.common.base.Preconditions.checkArgument; public final class DisputeClosedMessage extends DisputeMessage { private final DisputeResult disputeResult; private final NodeAddress senderNodeAddress; + @Nullable private final String updatedMultisigHex; @Nullable private final String unsignedPayoutTxHex; @@ -44,7 +45,7 @@ public final class DisputeClosedMessage extends DisputeMessage { NodeAddress senderNodeAddress, String uid, SupportType supportType, - String updatedMultisigHex, + @Nullable String updatedMultisigHex, @Nullable String unsignedPayoutTxHex, boolean deferPublishPayout) { this(disputeResult, @@ -85,9 +86,9 @@ public final class DisputeClosedMessage extends DisputeMessage { .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex) .setDeferPublishPayout(deferPublishPayout); Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); + Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build(); } @@ -98,7 +99,7 @@ public final class DisputeClosedMessage extends DisputeMessage { proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - proto.getUpdatedMultisigHex(), + ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), proto.getDeferPublishPayout()); } diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java index 8748def337..ad078e96ac 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java @@ -97,11 +97,11 @@ public final class RefundManager extends DisputeManager { message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeOpenedMessage) { - handleDisputeOpenedMessage((DisputeOpenedMessage) message); + handle((DisputeOpenedMessage) message); } else if (message instanceof ChatMessage) { - handleChatMessage((ChatMessage) message); + handle((ChatMessage) message); } else if (message instanceof DisputeClosedMessage) { - handleDisputeClosedMessage((DisputeClosedMessage) message); + handle((DisputeClosedMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } @@ -149,7 +149,7 @@ public final class RefundManager extends DisputeManager { @Override // We get that message at both peers. The dispute object is in context of the trader - public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) { + public void handle(DisputeClosedMessage disputeResultMessage) { DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); String tradeId = disputeResult.getTradeId(); ChatMessage chatMessage = disputeResult.getChatMessage(); @@ -162,7 +162,7 @@ public final class RefundManager extends DisputeManager { "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { // We delay 2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2); + Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2); delayMsgMap.put(uid, timer); } else { log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + diff --git a/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java b/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java index f462becdf0..34fb6ae98f 100644 --- a/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java @@ -32,6 +32,7 @@ import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; +import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import java.util.List; @@ -139,6 +140,19 @@ public class TraderChatManager extends SupportManager { @Override public void onAllServicesInitialized() { super.onAllServicesInitialized(); + + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onDataReceived() { + tryApplyMessages(); + } + }); + + xmrWalletService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { + if (xmrWalletService.isSyncedWithinTolerance()) + tryApplyMessages(); + }); + tryApplyMessages(); } @@ -148,7 +162,7 @@ public class TraderChatManager extends SupportManager { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof ChatMessage) { - handleChatMessage((ChatMessage) message); + handle((ChatMessage) message); } else { log.warn("Unsupported message at dispatchMessage. message={}", message); } diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java b/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java index db42547d45..cb5b0f1d6e 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableFormatter.java @@ -76,7 +76,7 @@ public class ClosedTradableFormatter { } public String getTotalTxFeeAsString(BigInteger totalTradeAmount, BigInteger totalTxFee) { - double percentage = HavenoUtils.divide(totalTxFee, totalTradeAmount); + double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTxFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TX_FEE, HavenoUtils.formatXmr(totalTxFee, true), formatToPercentWithSymbol(percentage)); @@ -104,7 +104,7 @@ public class ClosedTradableFormatter { } public String getTotalTradeFeeAsString(BigInteger totalTradeAmount, BigInteger totalTradeFee) { - double percentage = HavenoUtils.divide(totalTradeFee, totalTradeAmount); + double percentage = totalTradeAmount.equals(BigInteger.ZERO) ? 0 : HavenoUtils.divide(totalTradeFee, totalTradeAmount); return Res.get(I18N_KEY_TOTAL_TRADE_FEE_BTC, HavenoUtils.formatXmr(totalTradeFee, true), formatToPercentWithSymbol(percentage)); diff --git a/core/src/main/java/haveno/core/trade/Contract.java b/core/src/main/java/haveno/core/trade/Contract.java index 9a88eaff56..1e350d305b 100644 --- a/core/src/main/java/haveno/core/trade/Contract.java +++ b/core/src/main/java/haveno/core/trade/Contract.java @@ -261,18 +261,12 @@ public final class Contract implements NetworkPayload { } public boolean maybeClearSensitiveData() { - return false; // TODO: anything to clear? + return false; // nothing to clear } // edits a contract json string public static String sanitizeContractAsJson(String contractAsJson) { - return contractAsJson - .replaceAll( - "\"takerPaymentAccountPayload\": \\{[^}]*}", - "\"takerPaymentAccountPayload\": null") - .replaceAll( - "\"makerPaymentAccountPayload\": \\{[^}]*}", - "\"makerPaymentAccountPayload\": null"); + return contractAsJson; // nothing to sanitize because the contract does not contain the payment account payloads } public void printDiff(@Nullable String peersContractAsJson) { diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index d238d78843..984dd11a92 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -31,16 +31,20 @@ import haveno.common.file.FileUtil; import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; +import haveno.core.api.CorePaymentAccountsService; import haveno.core.api.XmrConnectionService; import haveno.core.app.HavenoSetup; +import haveno.core.locale.CurrencyUtil; import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.util.JsonUtil; +import haveno.core.xmr.wallet.XmrWalletBase; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; @@ -92,14 +96,18 @@ public class HavenoUtils { // configure fees public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true; - public static final double PENALTY_FEE_PCT = 0.02; // 2% - public static final double MAKER_FEE_PCT = 0.0015; // 0.15% - public static final double TAKER_FEE_PCT = 0.0075; // 0.75% - public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee when no deposit or fee from taker + public static final double PENALTY_FEE_PCT = 0.25; // charge 25% of security deposit for penalty + private static final double MAKER_FEE_PCT_CRYPTO = 0.0015; + private static final double TAKER_FEE_PCT_CRYPTO = 0.0075; + private static final double MAKER_FEE_PCT_TRADITIONAL = 0.0015; + private static final double TAKER_FEE_PCT_TRADITIONAL = 0.0075; + private static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_CRYPTO = MAKER_FEE_PCT_CRYPTO + TAKER_FEE_PCT_CRYPTO; // can customize maker's fee when no deposit from taker + private static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_TRADITIONAL = MAKER_FEE_PCT_TRADITIONAL + TAKER_FEE_PCT_TRADITIONAL; + public static final double MINER_FEE_TOLERANCE_FACTOR = 5.0; // miner fees must be within 5x of each other // other configuration public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes - public static final long LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s + public static final long LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s public static final int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase // synchronize requests to the daemon @@ -122,6 +130,7 @@ public class HavenoUtils { private static final BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000"); public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); + private static List bip39Words = new ArrayList(); // shared references TODO: better way to share references? public static HavenoSetup havenoSetup; @@ -130,6 +139,8 @@ public class HavenoUtils { public static XmrConnectionService xmrConnectionService; public static OpenOfferManager openOfferManager; public static CoreNotificationService notificationService; + public static CorePaymentAccountsService corePaymentAccountService; + public static TradeStatisticsManager tradeStatisticsManager; public static Preferences preferences; public static boolean isSeedNode() { @@ -144,11 +155,17 @@ public class HavenoUtils { @SuppressWarnings("unused") public static Date getReleaseDate() { if (RELEASE_DATE == null) return null; - try { - return DATE_FORMAT.parse(RELEASE_DATE); - } catch (Exception e) { - log.error("Failed to parse release date: " + RELEASE_DATE, e); - throw new IllegalArgumentException(e); + return parseDate(RELEASE_DATE); + } + + private static Date parseDate(String date) { + synchronized (DATE_FORMAT) { + try { + return DATE_FORMAT.parse(date); + } catch (Exception e) { + log.error("Failed to parse date: " + date, e); + throw new IllegalArgumentException(e); + } } } @@ -166,6 +183,26 @@ public class HavenoUtils { GenUtils.waitFor(waitMs); } + public static double getMakerFeePct(String currencyCode, boolean hasBuyerAsTakerWithoutDeposit) { + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + return hasBuyerAsTakerWithoutDeposit ? MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_CRYPTO : MAKER_FEE_PCT_CRYPTO; + } else if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { + return hasBuyerAsTakerWithoutDeposit ? MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT_TRADITIONAL : MAKER_FEE_PCT_TRADITIONAL; + } else { + throw new IllegalArgumentException("Unsupported currency code: " + currencyCode); + } + } + + public static double getTakerFeePct(String currencyCode, boolean hasBuyerAsTakerWithoutDeposit) { + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + return hasBuyerAsTakerWithoutDeposit ? 0d : TAKER_FEE_PCT_CRYPTO; + } else if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { + return hasBuyerAsTakerWithoutDeposit ? 0d : TAKER_FEE_PCT_TRADITIONAL; + } else { + throw new IllegalArgumentException("Unsupported currency code: " + currencyCode); + } + } + // ----------------------- CONVERSION UTILS ------------------------------- public static BigInteger coinToAtomicUnits(Coin coin) { @@ -297,10 +334,7 @@ public class HavenoUtils { try { // load bip39 words - String fileName = "bip39_english.txt"; - File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); - if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); - List bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + loadBip39Words(); // select words randomly List passphraseWords = new ArrayList(); @@ -314,13 +348,26 @@ public class HavenoUtils { } } + private static synchronized void loadBip39Words() { + if (bip39Words.isEmpty()) { + try { + String fileName = "bip39_english.txt"; + File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName); + if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File); + bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new IllegalStateException("Failed to load BIP39 words", e); + } + } + } + public static String getChallengeHash(String challenge) { if (challenge == null) return null; // tokenize passphrase String[] words = challenge.toLowerCase().split(" "); - // collect first 4 letters of each word, which are unique in bip39 + // collect up to first 4 letters of each word, which are unique in bip39 List prefixes = new ArrayList(); for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); @@ -580,17 +627,25 @@ public class HavenoUtils { } public static boolean isUnresponsive(Throwable e) { - return isConnectionRefused(e) || isReadTimeout(e); + return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e); } public static boolean isNotEnoughSigners(Throwable e) { return e != null && e.getMessage().contains("Not enough signers"); } + public static boolean isFailedToParse(Throwable e) { + return e != null && e.getMessage().contains("Failed to parse"); + } + public static boolean isTransactionRejected(Throwable e) { return e != null && e.getMessage().contains("was rejected"); } + public static boolean isLRNotFound(Throwable e) { + return e != null && e.getMessage().contains("LR not found for enough participants"); + } + public static boolean isIllegal(Throwable e) { return e instanceof IllegalArgumentException || e instanceof IllegalStateException; } @@ -650,4 +705,16 @@ public class HavenoUtils { } }).start(); } + + public static void verifyMinerFee(BigInteger expected, BigInteger actual) { + BigInteger max = expected.max(actual); + BigInteger min = expected.min(actual); + if (min.compareTo(BigInteger.ZERO) <= 0) { + throw new IllegalArgumentException("Miner fees must be greater than zero"); + } + double factor = divide(max, min); + if (factor > MINER_FEE_TOLERANCE_FACTOR) { + throw new IllegalArgumentException("Miner fees are not within " + MINER_FEE_TOLERANCE_FACTOR + "x of each other. Expected=" + expected + ", actual=" + actual + ", factor=" + factor); + } + } } diff --git a/core/src/main/java/haveno/core/trade/SellerTrade.java b/core/src/main/java/haveno/core/trade/SellerTrade.java index fae3cce7a1..6343ce1073 100644 --- a/core/src/main/java/haveno/core/trade/SellerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerTrade.java @@ -25,9 +25,13 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.math.BigInteger; +import java.util.Date; @Slf4j public abstract class SellerTrade extends Trade { + + private static final long resendPaymentReceivedMessagesDurationMs = 2L * 30 * 24 * 60 * 60 * 1000; // ~2 months + SellerTrade(Offer offer, BigInteger tradeAmount, long tradePrice, @@ -59,5 +63,22 @@ public abstract class SellerTrade extends Trade { public boolean confirmPermitted() { return true; } + + public boolean isFinished() { + return super.isFinished() && !needsToResendPaymentReceivedMessages(); + } + + public boolean needsToResendPaymentReceivedMessages() { + return !isShutDownStarted() && getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !getProcessModel().isPaymentReceivedMessagesReceived() && resendPaymentReceivedMessagesEnabled() && resendPaymentReceivedMessagesWithinDuration(); + } + + private boolean resendPaymentReceivedMessagesEnabled() { + return getOffer().getOfferPayload().getProtocolVersion() >= 2; + } + + public boolean resendPaymentReceivedMessagesWithinDuration() { + Date startDate = getMaxTradePeriodDate(); // TODO: preferably use the date when the payment receipt was confirmed + return new Date().getTime() <= (startDate.getTime() + resendPaymentReceivedMessagesDurationMs); + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9d4112ff3f..63e35f7a6f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -39,11 +39,13 @@ import com.google.protobuf.ByteString; import com.google.protobuf.Message; import haveno.common.ThreadUtils; import haveno.common.UserThread; +import haveno.common.config.Config; import haveno.common.crypto.Encryption; import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; +import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; import haveno.core.network.MessageState; @@ -62,10 +64,12 @@ import haveno.core.support.messages.ChatMessage; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.ProcessModel; import haveno.core.trade.protocol.ProcessModelServiceProvider; +import haveno.core.trade.protocol.SellerProtocol; import haveno.core.trade.protocol.TradeListener; import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.TradeProtocol; -import haveno.core.trade.statistics.TradeStatistics3; +import haveno.core.trade.statistics.TradeStatisticsManager; +import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletBase; @@ -74,12 +78,14 @@ import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.TorNetworkNode; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; @@ -118,12 +124,15 @@ import javax.annotation.Nullable; import javax.crypto.SecretKey; import java.math.BigInteger; import java.time.Clock; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -144,12 +153,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5; + public static final int NUM_BLOCKS_DEPOSITS_FINALIZED = 30; // ~1 hour before deposits are considered finalized + public static final int NUM_BLOCKS_PAYOUT_FINALIZED = Config.baseCurrencyNetwork().isTestnet() ? 60 : 720; // ~1 day before payout is considered finalized and multisig wallet deleted + public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds + private static final long IDLE_SYNC_PERIOD_MS = Config.baseCurrencyNetwork().isTestnet() ? 30000 : 1680000; // 28 minutes (monero's default connection timeout is 30 minutes on a local connection, so beyond this the wallets will disconnect) + private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours) protected final Object pollLock = new Object(); + private final Object removeTradeOnErrorLock = new Object(); protected static final Object importMultisigLock = new Object(); private boolean pollInProgress; private boolean restartInProgress; private Subscription protocolErrorStateSubscription; private Subscription protocolErrorHeightSubscription; + public static final String PROTOCOL_VERSION = "protocolVersion"; // key for extraDataMap in trade statistics + public BooleanProperty wasWalletPolled = new SimpleBooleanProperty(false); /////////////////////////////////////////////////////////////////////////////////////////// // Enums @@ -172,16 +189,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), PUBLISH_DEPOSIT_TX_REQUEST_FAILED(Phase.DEPOSIT_REQUESTED), - // deposit published + // deposits published ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED), DEPOSIT_TXS_SEEN_IN_NETWORK(Phase.DEPOSITS_PUBLISHED), - // deposit confirmed + // deposits confirmed DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN(Phase.DEPOSITS_CONFIRMED), - // deposit unlocked + // deposits unlocked DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN(Phase.DEPOSITS_UNLOCKED), + // deposits finalized + DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN(Phase.DEPOSITS_FINALIZED), + // payment sent BUYER_CONFIRMED_PAYMENT_SENT(Phase.PAYMENT_SENT), BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), @@ -234,6 +254,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { DEPOSITS_PUBLISHED, DEPOSITS_CONFIRMED, DEPOSITS_UNLOCKED, + DEPOSITS_FINALIZED, PAYMENT_SENT, PAYMENT_RECEIVED; @@ -257,7 +278,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { PAYOUT_UNPUBLISHED, PAYOUT_PUBLISHED, PAYOUT_CONFIRMED, - PAYOUT_UNLOCKED; + PAYOUT_UNLOCKED, + PAYOUT_FINALIZED; public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) { return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name()); @@ -320,7 +342,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isOpen() { - return isRequested() && !isClosed(); + return ordinal() >= DisputeState.DISPUTE_OPENED.ordinal() && !isClosed(); } public boolean isCloseRequested() { @@ -429,10 +451,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { transient private Long pollPeriodMs; transient private Long pollNormalStartTimeMs; - public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds - private static final long IDLE_SYNC_PERIOD_MS = 1680000; // 28 minutes (monero's default connection timeout is 30 minutes on a local connection, so beyond this the wallets will disconnect) - private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours) - // Mutable @Getter transient private boolean isInitialized; @@ -454,6 +472,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private long lockTime; @Setter private long startTime; // added for haveno + private final Object startTimeLock = new Object(); @Getter @Nullable private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT; @@ -636,18 +655,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); - // reset states if no ack receive + // reset states if not awaiting processing if (!isPayoutPublished()) { - // reset buyer's payment sent state if no ack receive - if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { - log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + // reset buyer's payment sent state + if (this instanceof BuyerTrade && (getState().ordinal() == Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() || getState() == State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)) { + log.warn("Resetting state of {} {} from {} to {} because sending PaymentSentMessage failed", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); } - - // reset seller's payment received state if no ack receive - if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { - log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + + // reset seller's payment received state + if (this instanceof SellerTrade && (getState().ordinal() == Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() || getState() == State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG)) { + log.warn("Resetting state of {} {} from {} to {} because sending PaymentReceivedMessage failed", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); resetToPaymentSentState(); } } @@ -662,12 +681,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.submitToPool(() -> { - if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling(); + if (newValue == Trade.Phase.DEPOSIT_REQUESTED) onDepositRequested(); if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished(); if (newValue == Trade.Phase.DEPOSITS_CONFIRMED) onDepositsConfirmed(); if (newValue == Trade.Phase.DEPOSITS_UNLOCKED) onDepositsUnlocked(); + if (newValue == Trade.Phase.DEPOSITS_FINALIZED) onDepositsFinalized(); if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent(); - if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); + if (isDepositsPublished() && !isPayoutFinalized()) updatePollPeriod(); if (isPaymentReceived()) { UserThread.execute(() -> { if (tradePhaseSubscription != null) { @@ -683,7 +703,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.submitToPool(() -> { - if (isPayoutPublished()) updatePollPeriod(); + updatePollPeriod(); // handle when payout published if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) { @@ -713,11 +733,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { processModel.getXmrWalletService().swapPayoutAddressEntryToAvailable(getId()); } - // handle when payout unlocks - if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) { + // handle when payout finalized + if (newValue == Trade.PayoutState.PAYOUT_FINALIZED) { if (!isInitialized) return; - log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); - if (isCompleted()) clearAndShutDown(); + log.info("Payout finalized for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); + if (isInitialized && isFinished()) clearAndShutDown(); else deleteWallet(); } }); @@ -739,7 +759,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed? + // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { @@ -753,8 +773,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { importMultisigHexIfScheduled(); }); - // done if deposit not requested or payout unlocked - if (!isDepositRequested() || isPayoutUnlocked()) { + // done if deposit not requested or payout finalized + if (!isDepositRequested() || isPayoutFinalized()) { isInitialized = true; isFullyInitialized = true; return; @@ -764,9 +784,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (walletExists()) getWallet(); else { MoneroTx payoutTx = getPayoutTx(); - if (payoutTx != null && payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { - log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState()); - setPayoutStateUnlocked(); + if (payoutTx != null) { + + // update payout state if necessary + if (!isPayoutUnlocked() && payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState()); + setPayoutStateUnlocked(); + } + if (!isPayoutFinalized() && payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) { + log.warn("Payout state for {} {} is {} but payout is finalized, updating state", getClass().getSimpleName(), getId(), getPayoutState()); + setPayoutStateFinalized(); + } isInitialized = true; isFullyInitialized = true; return; @@ -775,6 +803,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + // poll wallet without network calls + walletHeight.set(wallet.getHeight()); + doPollWallet(true); + // trade is initialized isInitialized = true; @@ -786,30 +818,63 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isFinished() { - return isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages(); + if (!isCompleted()) return false; + if (isPayoutUnlocked() && !walletExists()) return true; + return isPayoutFinalized(); } public void resetToPaymentSentState() { setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null); + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + } setPayoutTxHex(null); } - public void reprocessApplicableMessages() { - if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return; + public void initializeAfterMailboxMessages() { + if (!isDepositRequested() || isPayoutFinalized() || isCompleted()) return; getProtocol().maybeReprocessPaymentSentMessage(false); getProtocol().maybeReprocessPaymentReceivedMessage(false); HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + + // handle when wallet first polled + if (wasWalletPolled.get()) onWalletFirstPolled(); + else { + wasWalletPolled.addListener((observable, oldValue, newValue) -> { + if (newValue) onWalletFirstPolled(); + }); + } + } + + private void onWalletFirstPolled() { + requestSaveWallet(); + checkForUnconfirmedTimeout(); + } + + private void checkForUnconfirmedTimeout() { + if (isDepositsConfirmed()) return; + long unconfirmedHours = Duration.between(getDate().toInstant(), Instant.now()).toHours(); + if (unconfirmedHours >= 3 && !hasFailed()) { + String errorMessage = Res.get("portfolio.pending.unconfirmedTooLong", getShortId(), unconfirmedHours); + prependErrorMessage(errorMessage); + } } public void awaitInitialized() { while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling } + // TODO: throw if trade manager is null public void requestPersistence() { if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); } + // TODO: throw if trade manager is null + public void persistNow(@Nullable Runnable completeHandler) { + if (processModel.getTradeManager() != null) processModel.getTradeManager().persistNow(completeHandler); + } + public TradeProtocol getProtocol() { return processModel.getTradeManager().getTradeProtocol(this); } @@ -828,7 +893,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void setCompleted(boolean completed) { this.isCompleted = completed; - if (isPayoutUnlocked()) clearAndShutDown(); + if (isInitialized && isFinished()) clearAndShutDown(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -885,6 +950,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isIdling() { + if (isPayoutUnlocked()) return true; // idle after payout unlocked return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -1008,23 +1074,28 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { syncedWallet = true; } - // sync wallet if deposit requested and payout not unlocked - if (!isPayoutUnlocked() && !syncedWallet) { + // sync wallet if deposit requested and payout not finalized + if (!isPayoutFinalized() && !syncedWallet) { log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); syncWallet(true); } - // check if deposits published and payout not unlocked - if (isDepositsPublished() && !isPayoutUnlocked()) { - throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); + // check if deposits published and payout not finalized + if (isDepositsPublished() && !isPayoutFinalized()) { + throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not finalized"); } // check for balance if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId()); - wallet.rescanSpent(); + rescanSpent(false); if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance()); + if (isBuyer()) { + processBuyerPayout(payoutTxId); // process payout to main wallet + log.warn("Trade wallet for " + getClass().getSimpleName() + " " + getId() + " has a balance of " + wallet.getBalance() + ", but payout tx " + payoutTxId + " is verified, so proceeding to delete wallet"); + } else { + throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance()); + } } } } @@ -1038,7 +1109,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.deleteWalletBackups(getWalletName()); } catch (Exception e) { log.warn("Error deleting wallet for {} {}: {}\n", getClass().getSimpleName(), getId(), e.getMessage(), e); - setErrorMessage(e.getMessage()); + prependErrorMessage(e.getMessage()); processModel.getTradeManager().getNotificationService().sendErrorNotification("Error", e.getMessage()); } } else { @@ -1088,7 +1159,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { synchronized (HavenoUtils.getWalletFunctionLock()) { MoneroTxWallet tx = wallet.createTx(txConfig); exportMultisigHex(); - saveWallet(); return tx; } } @@ -1096,8 +1166,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void exportMultisigHex() { synchronized (walletLock) { + log.info("Exporting multisig info for {} {}", getClass().getSimpleName(), getShortId()); getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex()); - requestPersistence(); + saveWallet(); } } @@ -1116,8 +1187,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private void importMultisigHexIfScheduled() { if (!isInitialized || isShutDownStarted) return; - if (!isDepositsConfirmed() || getMaker().getDepositTx() == null) return; - if (walletHeight.get() - getMaker().getDepositTx().getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return; + MoneroTxWallet makerDepositTx = getMaker().getDepositTx(); + if (!isDepositsConfirmed() || makerDepositTx == null) return; + if (walletHeight.get() - makerDepositTx.getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return; ThreadUtils.execute(() -> { if (!isInitialized || isShutDownStarted) return; synchronized (getLock()) { @@ -1142,7 +1214,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw e; } catch (Exception e) { log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; @@ -1156,8 +1228,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private void doImportMultisigHex() { - // ensure wallet sees deposits confirmed - if (!isDepositsConfirmed()) syncAndPollWallet(); + // sync and poll wallet if deposits not confirmed (unless only one deposit unlocked) + if (!isDepositsConfirmed() && !hasUnlockedTx()) syncAndPollWallet(); // collect multisig hex from peers List multisigHexes = new ArrayList(); @@ -1175,22 +1247,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { String errorMessage = "Multisig import still needed for " + getClass().getSimpleName() + " " + getShortId() + " after already importing, multisigHexes=" + multisigHexes; log.warn(errorMessage); - // ignore multisig hex which is significantly shorter than others + // remove shortest multisig hex if applicable int maxLength = 0; - boolean removed = false; - for (String hex : multisigHexes) maxLength = Math.max(maxLength, hex.length()); - for (String hex : new ArrayList<>(multisigHexes)) { - if (hex.length() < maxLength / 2) { - String ignoringMessage = "Ignoring multisig hex from " + getMultisigHexRole(hex) + " for " + getClass().getSimpleName() + " " + getShortId() + " because it is too short, multisigHex=" + hex; - setErrorMessage(ignoringMessage); - log.warn(ignoringMessage); - multisigHexes.remove(hex); - removed = true; - } + String shortestMultisigHex = null; + for (String hex : multisigHexes) { + if (shortestMultisigHex == null || hex.length() < shortestMultisigHex.length()) shortestMultisigHex = hex; + if (hex.length() > maxLength) maxLength = hex.length(); + } + if (shortestMultisigHex.length() < maxLength) { + log.warn("Removing multisig hex from " + getMultisigHexRole(shortestMultisigHex) + " for " + getClass().getSimpleName() + " " + getShortId() + " because it's the shortest, multisigHex=" + shortestMultisigHex); + multisigHexes.remove(shortestMultisigHex); + wallet.importMultisigHex(multisigHexes.toArray(new String[0])); } - // re-import valid multisig hexes - if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); + // throw if multisig import still needed if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); } @@ -1227,10 +1297,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); } - private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { + private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while - if (!HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); - getWallet(); // re-open wallet + if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0 && !HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts + if (!isShutDownStarted) getWallet(); // re-open wallet } private String getMultisigHexRole(String multisigHex) { @@ -1261,11 +1331,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { - return doCreatePayoutTx(); + MoneroTxWallet unsignedPayoutTx = doCreatePayoutTx(); + log.info("Done creating unsigned payout tx for {} {}", getClass().getSimpleName(), getShortId()); + return unsignedPayoutTx; } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); @@ -1298,13 +1370,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); // create payout tx - MoneroTxWallet payoutTx = createTx(new MoneroTxConfig() + MoneroTxWallet payoutTx; + try { + payoutTx = createTx(new MoneroTxConfig() .setAccountIndex(0) .addDestination(buyerPayoutAddress, buyerPayoutAmount) .addDestination(sellerPayoutAddress, sellerPayoutAmount) .setSubtractFeeFrom(0, 1) // split tx fee .setRelay(false) .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); + } catch (Exception e) { + if (HavenoUtils.isLRNotFound(e)) throw new IllegalStateException(e); + else throw e; + } // update state BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2)); @@ -1327,7 +1405,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw e; } catch (Exception e) { if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); @@ -1358,7 +1436,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); @@ -1390,7 +1468,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); - if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown + if (payoutTxId == null) setPayoutTx(payoutTx); // update payout tx if id currently unknown // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1422,7 +1500,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); // update payout tx - updatePayout(payoutTx); + setPayoutTx(payoutTx); // check connection boolean doSign = sign && getPayoutTxHex() == null; @@ -1446,13 +1524,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? - log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); + log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getShortId()); saveWallet(); // save wallet before creating fee estimate tx MoneroTxWallet feeEstimateTx = createPayoutTx(); - BigInteger feeEstimate = feeEstimateTx.getFee(); - double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? - if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), payoutTx.getFee()); + log.info("Payout tx fee is within tolerance for {} {}", getClass().getSimpleName(), getShortId()); } // set signed payout tx hex @@ -1461,7 +1537,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // describe result describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); payoutTx = describedTxSet.getTxs().get(0); - updatePayout(payoutTx); + setPayoutTx(payoutTx); } // save trade state @@ -1476,13 +1552,42 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { setPayoutStatePublished(); } catch (Exception e) { if (!isPayoutPublished()) { - if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e)) throw new IllegalArgumentException(e); + if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e); throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId() + ", error=" + e.getMessage(), e); } } } } + /** + * In case there's a problem observing the payout tx (e.g. due to stale multisig state), + * peers can communicate the payout tx id. + * + * @param payoutTxId is the payout tx id to process + */ + public void processBuyerPayout(String payoutTxId) { + if (payoutTxId == null) throw new IllegalArgumentException("Payout tx id cannot be null"); + if (!isBuyer()) throw new IllegalStateException("Only buyer can process buyer payout tx for " + getClass().getSimpleName() + " " + getShortId()); + + // poll the main wallet + log.warn("Processing payout tx for {} {} by polling main wallet", getClass().getSimpleName(), getShortId()); + xmrWalletService.doPollWallet(true); + + // fetch payout tx from main wallet + MoneroTxWallet payoutTx = xmrWalletService.getWallet().getTx(payoutTxId); + if (payoutTx == null) throw new RuntimeException("Payout tx id " + payoutTxId + " not found for " + getClass().getSimpleName() + " " + getId()); + if (payoutTx.isFailed()) throw new RuntimeException("Payout tx " + payoutTxId + " is failed for " + getClass().getSimpleName() + " " + getId()); + + // verify incoming amount + BigInteger txCost = payoutTx.getFee(); + BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2)); + BigInteger expectedAmount = getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit); + if (!payoutTx.getIncomingAmount().equals(expectedAmount)) throw new IllegalStateException("Payout tx incoming amount is not deposit amount + trade amount - 1/2 tx costs, " + payoutTx.getIncomingAmount() + " vs " + getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit)); + + // update payout tx + setPayoutTx(payoutTx); + } + /** * Decrypt the peer's payment account payload using the given key. * @@ -1511,15 +1616,24 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } @Nullable - public MoneroTx getTakerDepositTx() { + public MoneroTxWallet getTakerDepositTx() { return getTaker().getDepositTx(); } @Nullable - public MoneroTx getMakerDepositTx() { + public MoneroTxWallet getMakerDepositTx() { return getMaker().getDepositTx(); } + private Long getMinDepositTxConfirmations() { + MoneroTxWallet makerDepositTx = getMakerDepositTx(); + if (makerDepositTx == null) return null; + if (hasBuyerAsTakerWithoutDeposit()) return makerDepositTx.getNumConfirmations(); + MoneroTxWallet takerDepositTx = getTakerDepositTx(); + if (takerDepositTx == null) return null; + return Math.min(makerDepositTx.getNumConfirmations(), takerDepositTx.getNumConfirmations()); + } + public void addAndPersistChatMessage(ChatMessage chatMessage) { synchronized (chatMessages) { if (!chatMessages.contains(chatMessage)) { @@ -1550,6 +1664,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public void clearAndShutDown() { + + // unregister p2p message listener immediately + removeDecryptedDirectMessageListener(); + + // clear process data and shut down trade ThreadUtils.execute(() -> { clearProcessData(); onShutDownStarted(); @@ -1566,23 +1685,45 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // TODO: clear other process data - setPayoutTxHex(null); + if (processModel.isPaymentReceivedMessagesReceived()) setPayoutTxHex(null); for (TradePeer peer : getAllPeers()) { - peer.setUnsignedPayoutTxHex(null); peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); - if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null); + peer.setDepositTxHex(null); + peer.setDepositTxKey(null); + if (peer.isPaymentReceivedMessageReceived()) { + peer.setUnsignedPayoutTxHex(null); + peer.setPaymentReceivedMessage(null); + } } } + private void removeDecryptedDirectMessageListener() { + if (getProcessModel() == null || getProcessModel().getProvider() == null || getProcessModel().getP2PService() == null) return; + getProcessModel().getP2PService().removeDecryptedDirectMessageListener(getProtocol()); + } + public void maybeClearSensitiveData() { String change = ""; + if (contract != null && contract.maybeClearSensitiveData()) { + change += "contract;"; + } + if (processModel != null && processModel.maybeClearSensitiveData()) { + change += "processModel;"; + } + if (contractAsJson != null) { + String edited = Contract.sanitizeContractAsJson(contractAsJson); + if (!edited.equals(contractAsJson)) { + contractAsJson = edited; + change += "contractAsJson;"; + } + } if (removeAllChatMessages()) { change += "chat messages;"; } if (change.length() > 0) { - log.info("cleared sensitive data from {} of trade {}", change, getShortId()); + log.info("Cleared sensitive data from {} of {} {}", change, getClass().getSimpleName(), getShortId()); } } @@ -1595,7 +1736,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void shutDown() { if (isShutDown) return; // ignore if already shut down isShutDownStarted = true; - if (!isPayoutUnlocked()) log.info("Shutting down {} {}", getClass().getSimpleName(), getId()); + if (!isPayoutFinalized()) log.info("Shutting down {} {}", getClass().getSimpleName(), getId()); + + // unregister p2p message listener + removeDecryptedDirectMessageListener(); // create task to shut down trade Runnable shutDownTask = () -> { @@ -1608,11 +1752,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // shut down trade threads - isInitialized = false; isShutDown = true; List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); ThreadUtils.awaitTasks(shutDownThreads); + stopProtocolTimeout(); + isInitialized = false; // save and close if (wallet != null) { @@ -1652,35 +1797,25 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // Trade error cleanup /////////////////////////////////////////////////////////////////////////////////////////// - public void onProtocolError() { + public void onProtocolInitializationError() { - // check if deposit published + // check if deposits published if (isDepositsPublished()) { restoreDepositsPublishedTrade(); return; } - // unreserve taker's key images - if (this instanceof TakerTrade) { - ThreadUtils.submitToPool(() -> { - xmrWalletService.thawOutputs(getSelf().getReserveTxKeyImages()); - }); - } - - // unreserve maker's open offer - Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); - if (this instanceof MakerTrade && openOffer.isPresent()) { - processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); - } - // remove if deposit not requested or is failed - if (!isDepositRequested() || isDepositFailed()) { + if (!isDepositRequested() || isDepositRequestFailed()) { removeTradeOnError(); return; } // done if wallet already deleted - if (!walletExists()) return; + if (!walletExists()) { + removeTradeOnError(); + return; + } // set error height if (processModel.getTradeProtocolErrorHeight() == 0) { @@ -1714,8 +1849,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> { // get trade's deposit txs from daemon - MoneroTx makerDepositTx = getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(getMaker().getDepositTxHash()); - MoneroTx takerDepositTx = getTaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(getTaker().getDepositTxHash()); + MoneroTx makerDepositTx = getMaker().getDepositTxHash() == null ? null : xmrWalletService.getMonerod().getTx(getMaker().getDepositTxHash()); + MoneroTx takerDepositTx = getTaker().getDepositTxHash() == null ? null : xmrWalletService.getMonerod().getTx(getTaker().getDepositTxHash()); // remove trade and wallet if neither deposit tx published if (makerDepositTx == null && takerDepositTx == null) { @@ -1765,24 +1900,43 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void removeTradeOnError() { - log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); + synchronized (removeTradeOnErrorLock) { - // force close and re-open wallet in case stuck - forceCloseWallet(); - if (isDepositRequested()) getWallet(); + // skip if already shut down or removed + if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return; + log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); - // shut down trade thread - try { - ThreadUtils.shutDown(getId(), 1000l); - } catch (Exception e) { - log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + // force close and re-open wallet in case stuck + forceCloseWallet(); + if (isDepositRequested()) getWallet(); + + // unreserve taker's key images + if (this instanceof TakerTrade) { + ThreadUtils.submitToPool(() -> { + xmrWalletService.thawOutputs(getSelf().getReserveTxKeyImages()); + }); + } + + // unreserve maker's open offer + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); + if (this instanceof MakerTrade && openOffer.isPresent()) { + processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); + } + + // clear and shut down trade + onShutDownStarted(); + clearAndShutDown(); + + // shut down trade thread + try { + ThreadUtils.shutDown(getId(), 5000l); + } catch (Exception e) { + log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } + + // unregister trade + processModel.getTradeManager().unregisterTrade(this); } - - // clear and shut down trade - clearAndShutDown(); - - // unregister trade - processModel.getTradeManager().unregisterTrade(this); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1824,10 +1978,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } + public void stopProtocolTimeout() { + if (!isInitialized) return; + TradeProtocol protocol = getProtocol(); + if (protocol == null) return; + protocol.stopTimeout(); + } + public void setState(State state) { if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades - log.info("Set new state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), state); + log.info("Set new state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), state); } if (state.getPhase().ordinal() < this.state.getPhase().ordinal()) { String message = "We got a state change to a previous phase (id=" + getShortId() + ").\n" + @@ -1836,11 +1997,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } this.state = state; - requestPersistence(); - UserThread.await(() -> { + + + persistNow(null); + UserThread.execute(() -> { stateProperty.set(state); phaseProperty.set(state.getPhase()); }); + + // automatically advance unlocked state to finalized if sufficient confirmations + if (state == State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN) { + Long minDepositTxConfirmations = getMinDepositTxConfirmations(); + if (minDepositTxConfirmations != null && minDepositTxConfirmations >= NUM_BLOCKS_DEPOSITS_FINALIZED) { + log.info("Auto-advancing state to {} for {} {} because deposits are unlocked and have at least {} confirmations", State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN, this.getClass().getSimpleName(), getShortId(), NUM_BLOCKS_DEPOSITS_FINALIZED); + setState(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); + } + } } public void advanceState(State state) { @@ -1859,23 +2031,23 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void setPayoutState(PayoutState payoutState) { if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades - log.info("Set new payout state for {} {}: {}", this.getClass().getSimpleName(), getId(), payoutState); + log.info("Set new payout state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), payoutState); } if (payoutState.ordinal() < this.payoutState.ordinal()) { String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" + - "Old payout state is: " + this.state + ". New payout state is: " + payoutState; + "Old payout state is: " + this.payoutState + ". New payout state is: " + payoutState; log.warn(message); } this.payoutState = payoutState; - requestPersistence(); - UserThread.await(() -> payoutStateProperty.set(payoutState)); + persistNow(null); + UserThread.execute(() -> payoutStateProperty.set(payoutState)); } public void setDisputeState(DisputeState disputeState) { if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades - log.info("Set new dispute state for {} {}: {}", this.getClass().getSimpleName(), getShortId(), disputeState); + log.info("Set new dispute state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), disputeState); } if (disputeState.ordinal() < this.disputeState.ordinal()) { String message = "We got a dispute state change to a previous state (id=" + getShortId() + ").\n" + @@ -1884,6 +2056,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } this.disputeState = disputeState; + persistNow(null); UserThread.execute(() -> { disputeStateProperty.set(disputeState); }); @@ -1918,37 +2091,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getVolumeProperty().set(getVolume()); } - public void updatePayout(MoneroTxWallet payoutTx) { - - // set payout tx fields - this.payoutTx = payoutTx; - payoutTxKey = payoutTx.getKey(); - payoutTxFee = payoutTx.getFee().longValueExact(); - payoutTxId = payoutTx.getHash(); - if ("".equals(payoutTxId)) payoutTxId = null; // tx id is empty until signed - - // set payout tx id in dispute(s) - for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); - - // set final payout amounts - if (isPaymentReceived()) { - BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2)); - getBuyer().setPayoutTxFee(splitTxFee); - getSeller().setPayoutTxFee(splitTxFee); - getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); - getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); - } else { - DisputeResult disputeResult = getDisputeResult(); - if (disputeResult != null) { - BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); - getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); - getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); - getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee())); - getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee())); - } - } - } - public DisputeResult getDisputeResult() { if (getDisputes().isEmpty()) return null; return getDisputes().get(getDisputes().size() - 1).getDisputeResultProperty().get(); @@ -1956,8 +2098,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { @Nullable public MoneroTx getPayoutTx() { - if (payoutTx == null) { - payoutTx = payoutTxId == null ? null : (this instanceof ArbitratorTrade) ? xmrWalletService.getDaemonTxWithCache(payoutTxId) : xmrWalletService.getTx(payoutTxId); + if (payoutTx == null && payoutTxId != null) { + if (this instanceof ArbitratorTrade) { + payoutTx = xmrWalletService.getDaemonTxWithCache(payoutTxId); + } else { + payoutTx = xmrWalletService.getTx(payoutTxId); + if (payoutTx == null) { + log.warn("Main wallet is missing payout tx for {} {}, fetching from daemon", getClass().getSimpleName(), getShortId()); + payoutTx = xmrWalletService.getDaemonTxWithCache(payoutTxId); + } + } } return payoutTx; } @@ -2053,6 +2203,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return offer.getDirection() == OfferDirection.BUY ? processModel.getTaker() : processModel.getMaker(); } + public TradePeer getOtherPeer(TradePeer peer) { + List peers = getAllPeers(); + if (!peers.remove(peer)) throw new IllegalArgumentException("Peer is not maker, taker, or arbitrator"); + if (!peers.remove(getSelf())) throw new IllegalStateException("Self is not maker, taker, or arbitrator"); + if (peers.size() != 1) throw new IllegalStateException("There should be exactly one other peer"); + return peers.get(0); + } + // get the taker if maker, maker if taker, null if arbitrator public TradePeer getTradePeer() { if (this instanceof MakerTrade) return processModel.getTaker(); @@ -2086,6 +2244,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.NACKED) return MessageState.NACKED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; @@ -2132,53 +2291,64 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - public Date getHalfTradePeriodDate() { - return new Date(getStartTime() + getMaxTradePeriod() / 2); - } + public void maybeUpdateTradePeriod() { + synchronized (startTimeLock) { + if (startTime > 0) return; // already set + if (getTakeOfferDate() == null) return; // trade not started yet + if (!isDepositsFinalized()) return; // deposits not finalized yet - public Date getMaxTradePeriodDate() { - return new Date(getStartTime() + getMaxTradePeriod()); - } + long now = System.currentTimeMillis(); + long tradeTime = getTakeOfferDate().getTime(); + MoneroDaemon monerod = xmrWalletService.getMonerod(); + if (monerod == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); - private long getMaxTradePeriod() { - return getOffer().getPaymentMethod().getMaxTradePeriod(); - } + // get finalize time of last deposit tx + long finalizeHeight = getDepositsFinalizedHeight(); + long finalizeTime = monerod.getBlockByHeight(finalizeHeight).getTimestamp() * 1000; - private long getStartTime() { - long now = System.currentTimeMillis(); - if (isDepositsConfirmed() && getTakeOfferDate() != null) { - if (isDepositsUnlocked()) { - if (startTime <= 0) setStartTimeFromUnlockedTxs(); // save to model - return startTime; - } else { - log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash()); - return now; - } - } else { - return now; + // If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date. + // If block date is earlier than our trade date we use our trade date. + if (finalizeTime > now) + startTime = now; + else + startTime = Math.max(finalizeTime, tradeTime); + + log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", + new Date(startTime), new Date(tradeTime), new Date(finalizeTime)); } } - private void setStartTimeFromUnlockedTxs() { - long now = System.currentTimeMillis(); - final long tradeTime = getTakeOfferDate().getTime(); - MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); - if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); - if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); + private long getDepositsFinalizedHeight() { + MoneroTxWallet makerDepositTx = getMakerDepositTx(); + MoneroTxWallet takerDepositTx = getTakerDepositTx(); + if (makerDepositTx == null || (takerDepositTx == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot get finalized height for trade " + getId() + " because its deposit tx is null. Is client connected to a daemon?"); + return Math.max(makerDepositTx.getHeight() + NUM_BLOCKS_DEPOSITS_FINALIZED - 1, hasBuyerAsTakerWithoutDeposit() ? 0l : takerDepositTx.getHeight() + NUM_BLOCKS_DEPOSITS_FINALIZED - 1); + } - // get unlock time of last deposit tx - long unlockHeight = Math.max(getMakerDepositTx().getHeight() + XmrWalletService.NUM_BLOCKS_UNLOCK - 1, hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight() + XmrWalletService.NUM_BLOCKS_UNLOCK - 1); - long unlockTime = daemonRpc.getBlockByHeight(unlockHeight).getTimestamp() * 1000; + public long getMaxTradePeriod() { + return getOffer().getPaymentMethod().getMaxTradePeriod(); + } - // If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date. - // If block date is earlier than our trade date we use our trade date. - if (unlockTime > now) - startTime = now; - else - startTime = Math.max(unlockTime, tradeTime); + public Date getHalfTradePeriodDate() { + return new Date(getEffectiveStartTime() + getMaxTradePeriod() / 2); + } - log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", - new Date(startTime), new Date(tradeTime), new Date(unlockTime)); + public Date getMaxTradePeriodDate() { + return new Date(getEffectiveStartTime() + getMaxTradePeriod()); + } + + public Date getStartDate() { + return new Date(getEffectiveStartTime()); + } + + /** + * Returns the effective start time for the trade period. + * Returns the current time until the deposits are finalized. + */ + private long getEffectiveStartTime() { + synchronized (startTimeLock) { + return startTime > 0 ? startTime : System.currentTimeMillis(); + } } public boolean hasFailed() { @@ -2189,21 +2359,35 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return getState().getPhase().ordinal() == Phase.INIT.ordinal(); } + public boolean isFundsLockedIn() { + return isDepositsPublished() && !isPayoutPublished(); + } + public boolean isDepositRequested() { return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal(); } - public boolean isDepositFailed() { + public boolean isDepositRequestFailed() { return getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED; } + public boolean isDepositTxMissing() { + if (!wasWalletPolled.get()) throw new IllegalStateException("Cannot determine if deposit tx is missing because wallet has not been polled"); + MoneroTxWallet makerDepositTx = getMakerDepositTx(); + MoneroTxWallet takerDepositTx = getTakerDepositTx(); + boolean hasUnlockedDepositTx = (makerDepositTx != null && Boolean.FALSE.equals(makerDepositTx.isLocked())) || (takerDepositTx != null && Boolean.FALSE.equals(takerDepositTx.isLocked())); + if (!hasUnlockedDepositTx) return false; + boolean hasMissingDepositTx = makerDepositTx == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx == null); + return hasMissingDepositTx; + } + public boolean isDepositsPublished() { - if (isDepositFailed()) return false; + if (isDepositRequestFailed()) return false; return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit()); } - public boolean isFundsLockedIn() { - return isDepositsPublished() && !isPayoutPublished(); + public boolean isDepositsSeen() { + return isDepositsPublished() && getState().ordinal() >= State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal(); } public boolean isDepositsConfirmed() { @@ -2224,10 +2408,31 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal(); } + public boolean isDepositsFinalized() { + if (getState().getPhase().ordinal() < Phase.DEPOSITS_FINALIZED.ordinal()) return false; + else if (getState().getPhase() == Phase.DEPOSITS_FINALIZED) return true; + else if (isPayoutFinalized()) return true; + else { + Long minDepositTxConfirmations = getMinDepositTxConfirmations(); + + // TODO: state can be past finalized (e.g. payment_sent) before the deposits are finalized, ideally use separate enum for deposits, or a single published state + num confirmations + if (minDepositTxConfirmations == null) { + log.warn("Assuming that deposit txs are finalized for trade {} {} because trade is in phase {} but has unknown confirmations", getClass().getSimpleName(), getShortId(), getState().getPhase()); + Thread.dumpStack(); + return true; + } + return minDepositTxConfirmations >= NUM_BLOCKS_DEPOSITS_FINALIZED; + } + } + public boolean isPaymentSent() { return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG; } + public boolean hasPaymentSentMessage() { + return (isBuyer() ? getSeller() : getBuyer()).getPaymentSentMessage() != null; // buyer stores message to seller and arbitrator, peers store message from buyer + } + public boolean hasPaymentReceivedMessage() { return (isSeller() ? getBuyer() : getSeller()).getPaymentReceivedMessage() != null; // seller stores message to buyer and arbitrator, peers store message from seller } @@ -2242,6 +2447,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return getDisputeState().isClosed(); } + public boolean isPaymentMarkedSent() { + return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); + } + + public boolean isPaymentMarkedReceived() { + return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); + } + public boolean isPaymentReceived() { return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG; } @@ -2258,6 +2471,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal(); } + public boolean isPayoutFinalized() { + return getPayoutState().ordinal() >= PayoutState.PAYOUT_FINALIZED.ordinal(); + } + public ReadOnlyDoubleProperty initProgressProperty() { return initProgressProperty; } @@ -2337,8 +2554,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return isBuyer() ? getBuyer().getSecurityDeposit() : getAmount().add(getSeller().getSecurityDeposit()); } + /** + * Returns the price as XMR/QUOTE. + */ public Price getPrice() { - return Price.valueOf(offer.getCurrencyCode(), price); + boolean isInverted = getOffer().isInverted(); // return uninverted price + return Price.valueOf(offer.getCounterCurrencyCode(), isInverted ? PriceUtil.invertLongPrice(price, offer.getCounterCurrencyCode()) : price); + } + + public Price getRawPrice() { + return Price.valueOf(offer.getCounterCurrencyCode(), price); } @Nullable @@ -2406,12 +2631,27 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public void maybePublishTradeStatistics() { - if (shouldPublishTradeStatistics()) doPublishTradeStatistics(); + if (shouldPublishTradeStatistics()) { + + // publish after random delay within 24 hours + UserThread.runAfterRandomDelay(() -> { + if (!isShutDownStarted) doPublishTradeStatistics(); + }, 0, TradeStatisticsManager.PUBLISH_STATS_RANDOM_DELAY_HOURS * 60 * 60 * 1000, TimeUnit.MILLISECONDS); + } } public boolean shouldPublishTradeStatistics() { - if (!isSeller()) return false; - return tradeAmountTransferred(); + + // do not publish if funds not transferred + if (!tradeAmountTransferred()) return false; + + // only seller or arbitrator publish trade stats + if (!isSeller() && !isArbitrator()) return false; + + // prior to v3 protocol, only seller publishes trade stats + if (getOffer().getOfferPayload().getProtocolVersion() < 3 && !isSeller()) return false; + + return true; } @@ -2420,19 +2660,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// private boolean tradeAmountTransferred() { - return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER); + return isPayoutPublished() && (isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER)); } private void doPublishTradeStatistics() { String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null); boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode; - TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode, true); - if (tradeStatistics.isValid()) { - log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId()); - processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true); - } else { - log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", getClass().getSimpleName(), getId(), tradeStatistics); - } + HavenoUtils.tradeStatisticsManager.maybePublishTradeStatistics(this, referralId, isTorNetworkNode); } // lazy initialization @@ -2488,17 +2722,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (isShutDownStarted) return; // set known deposit txs - List depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); - setDepositTxs(depositTxs); + doPollWallet(true); // start polling - if (!isIdling()) { - doTryInitSyncing(); - } else { - long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling + if (isIdling()) { + long startSyncingInSec = Math.max(1, ThreadLocalRandom.current().nextLong(0, getPollPeriod()) / 1000l); // random seconds to start polling UserThread.runAfter(() -> ThreadUtils.execute(() -> { if (!isShutDownStarted) doTryInitSyncing(); - }, getId()), startSyncingInMs / 1000l); + }, getId()), startSyncingInSec); + } else { + doTryInitSyncing(); } } @@ -2522,13 +2755,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { synchronized (walletLock) { - if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet() == null) throw new IllegalStateException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); if (isWalletBehind()) { - log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); + log.info("Syncing wallet for {} {} from height {}", getShortId(), getClass().getSimpleName(), walletHeight.get()); long startTime = System.currentTimeMillis(); syncWalletIfBehind(); - log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); + log.info("Done syncing wallet for {} {} in {} ms", getShortId(), getClass().getSimpleName(), System.currentTimeMillis() - startTime); } } @@ -2542,7 +2775,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (pollWallet) doPollWallet(); } catch (Exception e) { - if (!isShutDownStarted) ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + if (!(e instanceof IllegalStateException) && !isShutDownStarted) { + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + } + if (HavenoUtils.isUnresponsive(e)) { // wallet can be stuck a while + if (isShutDownStarted) forceCloseWallet(); + else forceRestartTradeWallet(); + } throw e; } } @@ -2602,6 +2841,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void doPollWallet() { + doPollWallet(false); + } + + private void doPollWallet(boolean offlinePoll) { // skip if shut down started if (isShutDownStarted) return; @@ -2614,130 +2857,97 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // poll wallet + MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { - // skip if payout unlocked - if (isPayoutUnlocked()) return; + // skip if shut down started + if (isShutDownStarted) return; - // skip if deposit txs unknown or not requested - if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; + // skip if payout finalized + if (isPayoutFinalized()) return; + + // skip if deposit txs unknown or not expected + if (!isDepositRequested() || isDepositRequestFailed() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if daemon not synced - if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; + if (!offlinePoll && (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance())) return; // sync if wallet too far behind daemon - if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); + if (!offlinePoll && walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); // update deposit txs - if (!isDepositsUnlocked()) { + boolean depositTxsUninitialized = isDepositRequested() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())); + if (depositTxsUninitialized || !isDepositsFinalized()) { // sync wallet if behind - syncWalletIfBehind(); + if (!offlinePoll) syncWalletIfBehind(); - // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && hasBuyerAsTakerWithoutDeposit())); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); - } - } - } - setDepositTxs(txs); - if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen - setStateDepositsSeen(); - - // set actual security deposits - if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); - getBuyer().setSecurityDeposit(buyerSecurityDeposit); - getSeller().setSecurityDeposit(sellerSecurityDeposit); - } - - // check for deposit txs confirmation - if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed(); - - // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { - setStateDepositsUnlocked(); + // set deposit txs from trade wallet + List txs = getTxs(false); + if (getValidMakerTx(txs) != null && (getValidTakerTx(txs) != null || hasBuyerAsTakerWithoutDeposit())) { + setDepositTxs(txs); + } else if (!offlinePoll) { + txs = getTxs(true); // check pool if deposits not found + setDepositTxs(txs); } } - // check for payout tx - if (isDepositsUnlocked()) { + // update payout tx + boolean hasUnlockedDeposit = hasUnlockedTx(); + if (isDepositsUnlocked() || hasUnlockedDeposit) { // arbitrator idles so these may not be the same // determine if payout tx expected boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); // sync wallet if payout expected or payout is published - if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); + if (!offlinePoll && (isPayoutExpected || isPayoutPublished())) syncWalletIfBehind(); // rescan spent outputs to detect unconfirmed payout tx - if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); + if (getPayoutState() == PayoutState.PAYOUT_PUBLISHED || (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0)) { try { - wallet.rescanSpent(); + rescanSpent(true); } catch (Exception e) { - log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); - ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); // do not block polling thread + ThreadUtils.submitToPool(() -> requestSwitchToNextBestConnection(sourceConnection)); // do not block polling thread } } // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs = null; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); + boolean checkPool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed(); + List txs = getTxs(checkPool); + setDepositTxs(txs); + + // update payout state + boolean hasPayoutTx = false; + MoneroTxWallet payoutTx = null; + for (MoneroTxWallet tx : txs) { + if (!Boolean.TRUE.equals(tx.isIncoming()) && !tx.isFailed()) { + payoutTx = tx; + hasPayoutTx = true; + break; + } else { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (Boolean.TRUE.equals(output.isSpent())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning) } } } - setDepositTxs(txs); - - // check if any outputs spent (observed on payout published) - boolean hasSpentOutput = false; - boolean hasFailedTx = false; - for (MoneroTxWallet tx : txs) { - if (tx.isFailed()) hasFailedTx = true; - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; - } - } - if (hasSpentOutput) setPayoutStatePublished(); - else if (hasFailedTx && isPayoutPublished()) { - log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); - setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); - } - - // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) - for (MoneroTxWallet tx : txs) { - if (tx.isOutgoing() && !tx.isFailed()) { - updatePayout(tx); - setPayoutStatePublished(); - if (tx.isConfirmed()) setPayoutStateConfirmed(); - if (!tx.isLocked()) setPayoutStateUnlocked(); - } - } + if (payoutTx != null) setPayoutTx(payoutTx); + else if (hasPayoutTx) setPayoutStatePublished(); + else if (checkPool && isPayoutPublished()) onPayoutUnseen(); // payout tx seen then lost (e.g. reorg) } } catch (Exception e) { - if (HavenoUtils.isUnresponsive(e)) { + if (!(e instanceof IllegalStateException) && !isShutDownStarted && !wasWalletPolled.get()) { // request connection switch if failure on first poll + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + } + if (HavenoUtils.isUnresponsive(e)) { // wallet can be stuck a while + if (wallet != null && !isShutDownStarted) { + log.warn("Error polling unresponsive trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection()); + } if (isShutDownStarted) forceCloseWallet(); else forceRestartTradeWallet(); - } - else { + } else { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection()); - //e.printStackTrace(); } } } finally { @@ -2746,20 +2956,110 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { pollInProgress = false; } } + wasWalletPolled.set(true); saveWalletWithDelay(); } } + private List getTxs(boolean checkPool) { + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + if (!checkPool) query.setInTxPool(false); // avoid checking pool if possible + List txs = null; + if (!checkPool) txs = wallet.getTxs(query); + else { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } + } + } + return txs; + } + + private void onPayoutUnseen() { + log.warn("Payout tx unseen for {} {} with payout state {}. Possible reorg?", getClass().getSimpleName(), getShortId(), getPayoutState()); + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + peer.setDisputeClosedMessage(null); + } + setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); + if (isCompleted()) processModel.getTradeManager().onMoveClosedTradeToPendingTrades(this); + String errorMsg = "The payout transaction is not seen for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the payout does not confirm automatically, you can contact support or mark the trade as failed."; + if (isSeller() && getState().ordinal() >= State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because payout is unpublished", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + setState(State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + onPayoutError(false, true, null); + setErrorMessage(errorMsg); + } else if (getState().ordinal() >= State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because payout is unpublished", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setErrorMessage(errorMsg); + } + } + + /** + * Handle a payout error due to NACK or the transaction failing (e.g. due to reorg). + * + * @param syncAndPoll whether to sync and poll + * @param resendPaymentReceivedMessages whether to resend payment received messages if previously confirmed + * @param paymentReceivedNackSender the peer that sent the payment received NACK, or null if not applicable + * @return true if the payment received were resent, false otherwise + */ + public boolean onPayoutError(boolean syncAndPoll, boolean resendPaymentReceivedMessages, TradePeer paymentReceivedNackSender) { + log.warn("Handling payout error for {} {}", getClass().getSimpleName(), getId()); + if (syncAndPoll) { + try { + syncAndPollWallet(); + } catch (Exception e) { + log.warn("Error syncing and polling wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } + } + + // reset trade state + log.warn("Resetting trade state after payout error for {} {}, nackSender={}", getClass().getSimpleName(), getId(), paymentReceivedNackSender == null ? null : getPeerRole(paymentReceivedNackSender)); + processModel.setPaymentSentPayoutTxStale(true); + if (paymentReceivedNackSender != null) { + paymentReceivedNackSender.setPaymentReceivedMessage(null); + paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.UNDEFINED); + } + if (!isPayoutPublished()) { + getSelf().setUnsignedPayoutTxHex(null); + setPayoutTxHex(null); + setPayoutTxId(null); + } + + persistNow(null); + + // send updated payment received message when payout is confirmed + if (resendPaymentReceivedMessages) { + if (!isSeller()) throw new IllegalArgumentException("Only the seller can resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId()); + if (!isPaymentReceived()) throw new IllegalStateException("Cannot resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId() + " because payment not marked received"); + log.warn("Sending updated PaymentReceivedMessages for {} {} after payout error", getClass().getSimpleName(), getId()); + ((SellerProtocol) getProtocol()).onPaymentReceived(() -> { + log.info("Done sending updated PaymentReceivedMessages on payout error for {} {}", getClass().getSimpleName(), getId()); + }, (errorMessage) -> { + log.warn("Error sending updated PaymentReceivedMessages on payout error for {} {}: {}", getClass().getSimpleName(), getId(), errorMessage); + }); + return true; + } + return false; + } + + private static boolean isUnlocked(MoneroTx tx) { + if (tx == null) return false; + if (tx.getNumConfirmations() == null || tx.getNumConfirmations() < XmrWalletService.NUM_BLOCKS_UNLOCK) return false; + return true; + } + + private boolean hasUnlockedTx() { + return isUnlocked(getMaker().getDepositTx()) || isUnlocked(getTaker().getDepositTx()); + } + private void syncWalletIfBehind() { synchronized (walletLock) { if (isWalletBehind()) { - - // TODO: local tests have timing failures unless sync called directly - if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) { - xmrWalletService.syncWallet(wallet); - } else { - syncWithProgress(); - } + syncWithProgress(); walletHeight.set(wallet.getHeight()); } } @@ -2770,15 +3070,156 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void setDepositTxs(List txs) { - for (MoneroTxWallet tx : txs) { - if (tx.getHash().equals(getMaker().getDepositTxHash())) getMaker().setDepositTx(tx); - if (tx.getHash().equals(getTaker().getDepositTxHash())) getTaker().setDepositTx(tx); + + // set deposit txs + getMaker().setDepositTx(getValidMakerTx(txs)); + getTaker().setDepositTx(getValidTakerTx(txs)); + + // set actual buyer security deposit + if (isSeen(getBuyer().getDepositTx())) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + if (!getBuyer().getSecurityDeposit().equals(BigInteger.ZERO) && !buyerSecurityDeposit.equals(getBuyer().getSecurityDeposit())) { + log.warn("Overwriting buyer security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getBuyer().getSecurityDeposit(), buyerSecurityDeposit); + } + getBuyer().setSecurityDeposit(buyerSecurityDeposit); } + + // set actual seller security deposit + if (isSeen(getSeller().getDepositTx())) { + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); + if (!getSeller().getSecurityDeposit().equals(BigInteger.ZERO) && !sellerSecurityDeposit.equals(getSeller().getSecurityDeposit())) { + log.warn("Overwriting seller security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getSeller().getSecurityDeposit(), sellerSecurityDeposit); + } + getSeller().setSecurityDeposit(sellerSecurityDeposit); + } + + // advance deposit state + if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) { + setStateDepositsSeen(); + + // check for deposit txs confirmed + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) { + setStateDepositsConfirmed(); + } + + // check for deposit txs unlocked + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { + setStateDepositsUnlocked(); + } + + // check for deposit txs finalized + if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) { + setStateDepositsFinalized(); + } + } + + // revert deposit state if necessary + State depositsState = getDepositsState(); + if (!isPaymentSent() && depositsState.ordinal() < getState().ordinal()) { + log.warn("Reverting deposits state to {} for {} {}. Possible reorg?", depositsState, getClass().getSimpleName(), getShortId()); + if (depositsState == State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS) setErrorMessage("Deposit transactions are missing for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the issue continues, you can contact support or mark the trade as failed."); + setState(depositsState); + } + + // announce deposits update depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); } - // TODO: wallet is sometimes missing balance or deposits, due to specific daemon connections, not saving? - private void recoverIfMissingWalletData() { + private MoneroTxWallet getValidMakerTx(List txs) { + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(getMaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { + return tx; + } + } + return null; + } + + private MoneroTxWallet getValidTakerTx(List txs) { + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(getTaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { + return tx; + } + } + return null; + } + + private static boolean isSeen(MoneroTx tx) { + if (tx == null) return false; + if (Boolean.TRUE.equals(tx.isFailed())) return false; + if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false; + return true; + } + + private State getDepositsState() { + if (getMaker().getDepositTx() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (getMaker().getDepositTx().isFailed() || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().isFailed())) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (getMaker().getDepositTx().getNumConfirmations() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().getNumConfirmations() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) return State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN; + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) return State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) return State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN; + if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) return State.DEPOSIT_TXS_SEEN_IN_NETWORK; + return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + } + + public void setPayoutTx(MoneroTx payoutTx) { + + // set payout tx fields + this.payoutTx = payoutTx; + this.payoutTxId = payoutTx.getHash(); + this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact(); + this.payoutTxKey = payoutTx.getKey(); + if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed + + // set payout tx id in dispute(s) + for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); + + // set final payout amounts + if (isPaymentReceived()) { + BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2)); + getBuyer().setPayoutTxFee(splitTxFee); + getSeller().setPayoutTxFee(splitTxFee); + getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); + getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); + } else { + DisputeResult disputeResult = getDisputeResult(); + if (disputeResult != null) { + BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); + getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); + getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); + getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee())); + getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee())); + } + } + + // advance payout state + if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) setPayoutStatePublished(); + if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); + if (payoutTx.getNumConfirmations() != null) { + if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked(); + if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized(); + } + + // revert payout state if necessary + PayoutState payoutState = getPayoutState(payoutTx); + if (payoutState.ordinal() < getPayoutState().ordinal()) { + log.warn("Reverting payout state to {} for {} {}. Possible reorg?", payoutState, getClass().getSimpleName(), getShortId()); + setPayoutState(payoutState); + } + } + + private static PayoutState getPayoutState(MoneroTx payoutTx) { + if (payoutTx.getHash() == null) return PayoutState.PAYOUT_UNPUBLISHED; + if (Boolean.TRUE.equals(payoutTx.isFailed())) return PayoutState.PAYOUT_UNPUBLISHED; + if (payoutTx.getNumConfirmations() != null) { + if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) return PayoutState.PAYOUT_FINALIZED; + if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) return PayoutState.PAYOUT_UNLOCKED; + } + if (payoutTx.isConfirmed()) return PayoutState.PAYOUT_CONFIRMED; + return PayoutState.PAYOUT_PUBLISHED; // payout is published by default in the wallet + } + + // TODO: wallet is sometimes missing balance or deposits, due to reorgs, specific daemon connections, not saving? + public void recoverIfMissingWalletData() { synchronized (walletLock) { if (isWalletMissingData()) { log.warn("Wallet is missing data for {} {}, attempting to recover", getClass().getSimpleName(), getShortId()); @@ -2789,32 +3230,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // skip if payout published in the meantime if (isPayoutPublished()) return; - // rescan blockchain with global daemon lock - synchronized (HavenoUtils.getDaemonLock()) { - Long timeout = null; - try { - - // extend rpc timeout for rescan - if (wallet instanceof MoneroWalletRpc) { - timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); - ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); - } - - // rescan blockchain - log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); - wallet.rescanBlockchain(); - } catch (Exception e) { - log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); - if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while - throw e; - } finally { - - // restore rpc timeout - if (wallet instanceof MoneroWalletRpc) { - ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); - } - } - } + // rescan blockchain + rescanBlockchain(); // import multisig hex log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId()); @@ -2829,6 +3246,70 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + public void rescanBlockchain() { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + if (getWallet() == null) throw new IllegalStateException("Cannot rescan blockchain because trade wallet doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot rescan blockchain because trade wallet is not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); + Long timeout = null; + try { + + // extend rpc timeout for rescan + if (wallet instanceof MoneroWalletRpc) { + timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); + } + + // rescan blockchain + log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); + wallet.rescanBlockchain(); + } catch (Exception e) { + log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while + throw e; + } finally { + + // restore rpc timeout + if (wallet instanceof MoneroWalletRpc) { + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); + } + } + } + } + } + + public void rescanSpent(boolean skipLog) { + synchronized (walletLock) { + if (getWallet() == null) throw new IllegalStateException("Cannot rescan spent outputs because trade wallet doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot rescan spent outputs because trade wallet is not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); + Long timeout = null; + try { + + // extend rpc timeout for rescan + if (wallet instanceof MoneroWalletRpc) { + timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); + } + + // rescan spent outputs + if (!skipLog) log.info("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId()); + wallet.rescanSpent(); + if (!skipLog) log.info("Done rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId()); + saveWalletWithDelay(); + } catch (Exception e) { + log.warn("Error rescanning spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while + throw e; + } finally { + + // restore rpc timeout + if (wallet instanceof MoneroWalletRpc) { + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); + } + } + } + } + private boolean isWalletMissingData() { synchronized (walletLock) { if (!isDepositsUnlocked() || isPayoutPublished()) return false; @@ -2854,15 +3335,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (isShutDownStarted || restartInProgress) return; log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); restartInProgress = true; + stopPolling(); forceCloseWallet(); if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; - pollWallet(); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitSyncing(), getId()); } private void setStateDepositsSeen() { - if (!isDepositsPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); + if (getState().ordinal() < State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); } private void setStateDepositsConfirmed() { @@ -2870,9 +3351,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void setStateDepositsUnlocked() { - if (!isDepositsUnlocked()) { - setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); - setStartTimeFromUnlockedTxs(); + if (!isDepositsUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + } + + private void setStateDepositsFinalized() { + if (!isDepositsFinalized()) { + setStateIfValidTransitionTo(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); + ThreadUtils.submitToPool(() -> maybeUpdateTradePeriod()); } } @@ -2888,6 +3373,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED); } + private void setPayoutStateFinalized() { + if (!isPayoutFinalized()) setPayoutState(PayoutState.PAYOUT_FINALIZED); + } + private Trade getTrade() { return this; } @@ -2908,8 +3397,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (processing) return; processing = true; - // skip if not idling and not waiting for payout to unlock - if (!isIdling() || !isPayoutPublished() || isPayoutUnlocked()) { + // skip if not idling and not waiting for payout to finalize + if (!isIdling() || !isPayoutPublished() || isPayoutFinalized()) { processing = false; return; } @@ -2918,14 +3407,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // get payout height if unknown if (payoutHeight == null && getPayoutTxId() != null && isPayoutPublished()) { - MoneroTx tx = xmrWalletService.getDaemon().getTx(getPayoutTxId()); + MoneroTx tx = xmrWalletService.getMonerod().getTx(getPayoutTxId()); if (tx == null) log.warn("Payout tx not found for {} {}, txId={}", getTrade().getClass().getSimpleName(), getId(), getPayoutTxId()); else if (tx.isConfirmed()) payoutHeight = tx.getHeight(); } // sync wallet if confirm or unlock expected - long currentHeight = xmrWalletService.getDaemon().getHeight(); - if (!isPayoutConfirmed() || (payoutHeight != null && currentHeight >= payoutHeight + XmrWalletService.NUM_BLOCKS_UNLOCK)) { + long currentHeight = xmrWalletService.getMonerod().getHeight(); + if (!isPayoutConfirmed() || (payoutHeight != null && + ((!isPayoutUnlocked() && currentHeight >= payoutHeight + XmrWalletService.NUM_BLOCKS_UNLOCK) || + (!isPayoutFinalized() && currentHeight >= payoutHeight + NUM_BLOCKS_PAYOUT_FINALIZED)))) { log.info("Syncing idle trade wallet to update payout tx, tradeId={}", getId()); syncAndPollWallet(); } @@ -2941,10 +3432,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + private void onDepositRequested() { + if (!isArbitrator()) startPolling(); // peers start polling after deposits requested + } + private void onDepositsPublished() { - // skip if arbitrator - if (this instanceof ArbitratorTrade) return; + // arbitrator starts polling after deposits published + if (isArbitrator()) { + startPolling(); + return; + } // close open offer or reset address entries if (this instanceof MakerTrade) { @@ -2966,6 +3464,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_UNLOCKED, "Trade Deposits Unlocked", "The deposit transactions have unlocked"); } + private void onDepositsFinalized() { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_FINALIZED, "Trade Deposits Finalized", "The deposit transactions have finalized"); + } + private void onPaymentSent() { HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 14ac565c26..95380554c5 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -108,6 +108,7 @@ import haveno.network.p2p.network.TorNetworkNode; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -138,7 +139,9 @@ import org.slf4j.LoggerFactory; public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener { + private static final Logger log = LoggerFactory.getLogger(TradeManager.class); + private static final int INIT_TRADE_RANDOM_DELAY_MS = 10000; // random delay to initialize trades private boolean isShutDownStarted; private boolean isShutDown; @@ -169,7 +172,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private final PersistenceManager> persistenceManager; private final TradableList tradableList = new TradableList<>(); @Getter - private final BooleanProperty persistedTradesInitialized = new SimpleBooleanProperty(); + private final BooleanProperty tradesInitialized = new SimpleBooleanProperty(); @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); private final ReferralIdService referralIdService; @@ -316,12 +319,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public void onAllServicesInitialized() { if (p2PService.isBootstrapped()) { - initPersistedTrades(); + initTrades(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onDataReceived() { - initPersistedTrades(); + initTrades(); } }); } @@ -332,13 +335,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Override public void onAccountCreated() { log.info(TradeManager.class + ".accountService.onAccountCreated()"); - initPersistedTrades(); + initTrades(); } @Override public void onAccountOpened() { log.info(TradeManager.class + ".accountService.onAccountOpened()"); - initPersistedTrades(); + initTrades(); } @Override @@ -423,11 +426,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } /////////////////////////////////////////////////////////////////////////////////////////// - // Init pending trade + // Init trades /////////////////////////////////////////////////////////////////////////////////////////// - private void initPersistedTrades() { - log.info("Initializing persisted trades"); + private void initTrades() { + log.info("Initializing trades"); // initialize off main thread new Thread(() -> { @@ -437,54 +440,35 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // initialize trades in parallel int threadPoolSize = 10; - Set tasks = new HashSet(); + Set initTradeTasks = new HashSet(); Set uids = new HashSet(); Set tradesToSkip = new HashSet(); Set uninitializedTrades = new HashSet(); for (Trade trade : trades) { - tasks.add(() -> { - try { - - // check for duplicate uid - if (!uids.add(trade.getUid())) { - log.warn("Found trade with duplicate uid, skipping. That should never happen. {} {}, uid={}", trade.getClass().getSimpleName(), trade.getId(), trade.getUid()); - tradesToSkip.add(trade); - return; - } - - // skip if failed and error handling not scheduled - if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) { - log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId()); - tradesToSkip.add(trade); - return; - } - - // initialize trade - initPersistedTrade(trade); - - // record if protocol didn't initialize - if (!trade.isDepositsPublished()) { - uninitializedTrades.add(trade); - } - } catch (Exception e) { - if (!isShutDownStarted) { - log.warn("Error initializing {} {}: {}\n", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); - trade.setInitError(e); - trade.prependErrorMessage(e.getMessage()); - } - } - }); + initTradeTasks.add(getInitTradeTask(trade, trades, tradesToSkip, uninitializedTrades, uids)); }; - ThreadUtils.awaitTasks(tasks, threadPoolSize); - log.info("Done initializing persisted trades"); + ThreadUtils.awaitTasks(initTradeTasks, threadPoolSize); + log.info("Done initializing trades"); if (isShutDownStarted) return; // remove skipped trades trades.removeAll(tradesToSkip); - // sync idle trades once in background after active trades + // arbitrator syncs idle trades once in background after active trades for (Trade trade : trades) { - if (trade.isIdling()) ThreadUtils.submitToPool(() -> trade.syncAndPollWallet()); + if (!trade.isArbitrator()) continue; + if (trade.isIdling()) { + ThreadUtils.submitToPool(() -> { + + // add random delay to avoid syncing at exactly the same time + if (trades.size() > 1 && trade.walletExists()) { + int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS); + HavenoUtils.waitFor(delay); + } + + trade.syncAndPollWallet(); + }); + } } // process after all wallets initialized @@ -492,7 +476,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // handle uninitialized trades for (Trade trade : uninitializedTrades) { - trade.onProtocolError(); + trade.onProtocolInitializationError(); } // freeze or thaw outputs @@ -513,7 +497,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // notify that persisted trades initialized if (isShutDownStarted) return; - persistedTradesInitialized.set(true); + tradesInitialized.set(true); getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); onTradesChanged(); @@ -523,14 +507,61 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi nonFailedTrades.addAll(tradableList.getList()); String referralId = referralIdService.getOptionalReferralId().orElse(null); boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; - tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); + tradeStatisticsManager.maybePublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); }).start(); // allow execution to start HavenoUtils.waitFor(100); } - private void initPersistedTrade(Trade trade) { + private Runnable getInitTradeTask(Trade trade, Collection trades, Set tradesToSkip, Set uninitializedTrades, Set uids) { + return () -> { + try { + + // check for duplicate uid + synchronized (uids) { + if (!uids.add(trade.getUid())) { + log.warn("Found trade with duplicate uid, skipping. That should never happen. {} {}, uid={}", trade.getClass().getSimpleName(), trade.getId(), trade.getUid()); + tradesToSkip.add(trade); + return; + } + } + + // skip if failed and error handling not scheduled + if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) { + log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId()); + synchronized (tradesToSkip) { + tradesToSkip.add(trade); + return; + } + } + + // add random delay to avoid syncing at exactly the same time + if (trades.size() > 1 && trade.walletExists()) { + int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS); + HavenoUtils.waitFor(delay); + } + + // initialize trade + initTrade(trade); + + // record if protocol didn't initialize + if (!trade.isDepositsPublished()) { + synchronized (uninitializedTrades) { + uninitializedTrades.add(trade); + } + } + } catch (Exception e) { + if (!isShutDownStarted) { + log.warn("Error initializing {} {}: {}\n", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); + trade.setInitError(e); + trade.prependErrorMessage(e.getMessage()); + } + } + }; + } + + private void initTrade(Trade trade) { if (isShutDown) return; if (getTradeProtocol(trade) != null) return; initTradeAndProtocol(trade, createTradeProtocol(trade)); @@ -546,6 +577,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi persistenceManager.requestPersistence(); } + public void persistNow(@Nullable Runnable completeHandler) { + persistenceManager.persistNow(completeHandler); + } + private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); @@ -563,9 +598,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi Optional openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); if (!openOfferOptional.isPresent()) return; OpenOffer openOffer = openOfferOptional.get(); - if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; Offer offer = openOffer.getOffer(); + // check availability + if (openOffer.getState() != OpenOffer.State.AVAILABLE) { + log.warn("Ignoring InitTradeRequest to maker because offer is not available, offerId={}, sender={}", request.getOfferId(), sender); + return; + } + // validate challenge if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); @@ -621,7 +661,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // process with protocol ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Maker error during trade initialization: " + errorMessage); - trade.onProtocolError(); + trade.onProtocolInitializationError(); }); } @@ -674,7 +714,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (!sender.equals(request.getTakerNodeAddress())) { if (sender.equals(request.getMakerNodeAddress())) { log.warn("Received InitTradeRequest from maker to arbitrator for trade that is already initializing, tradeId={}, sender={}", request.getOfferId(), sender); - sendAckMessage(sender, trade.getMaker().getPubKeyRing(), request, false, "Trade is already initializing for " + getClass().getSimpleName() + " " + trade.getId()); + sendAckMessage(sender, trade.getMaker().getPubKeyRing(), request, false, "Trade is already initializing for " + getClass().getSimpleName() + " " + trade.getId(), null); } else { log.warn("Ignoring InitTradeRequest from non-taker, tradeId={}, sender={}", request.getOfferId(), sender); } @@ -715,7 +755,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // process with protocol ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); - trade.onProtocolError(); + trade.onProtocolInitializationError(); }); requestPersistence(); @@ -880,6 +920,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // ensure trade is not already open Optional tradeOptional = getOpenTrade(offer.getId()); if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open"); + + // ensure failed trade is not processing + tradeOptional = getFailedTrade(offer.getId()); + if (tradeOptional.isPresent() && tradeOptional.get().walletExists()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " has failed but is not processed"); // create trade Trade trade; @@ -927,7 +971,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); - trade.onProtocolError(); + trade.onProtocolInitializationError(); xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling errorMessageHandler.handleErrorMessage(errorMessage); }); @@ -979,30 +1023,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (trade.isCompleted()) throw new RuntimeException("Trade " + trade.getId() + " was already completed"); closedTradableManager.add(trade); trade.setCompleted(true); - removeTrade(trade, true); - - // TODO The address entry should have been removed already. Check and if its the case remove that. - xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); + removeTrade(trade); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } public void unregisterTrade(Trade trade) { log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId()); - removeTrade(trade, true); + removeTrade(trade); removeFailedTrade(trade); + if (!trade.isMaker()) xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } - public void removeTrade(Trade trade, boolean removeDirectMessageListener) { + public void removeTrade(Trade trade) { log.info("TradeManager.removeTrade() " + trade.getId()); // remove trade synchronized (tradableList.getList()) { if (!tradableList.remove(trade)) return; } - - // unregister message listener and persist - if (removeDirectMessageListener) p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); requestPersistence(); } @@ -1034,16 +1074,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Override public void onMinuteTick() { - updateTradePeriodState(); + ThreadUtils.submitToPool(() -> updateTradePeriodState()); // update trade period off main thread } }); } + // TODO: could use monerod.getBlocksByHeight() to more efficiently update trade period state private void updateTradePeriodState() { if (isShutDownStarted) return; - synchronized (tradableList.getList()) { - for (Trade trade : tradableList.getList()) { - if (!trade.isInitialized() || trade.isPayoutPublished()) continue; + for (Trade trade : getOpenTrades()) { + if (!trade.isInitialized() || trade.isPayoutPublished()) continue; + try { + trade.maybeUpdateTradePeriod(); Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); if (maxTradePeriodDate != null && halfTradePeriodDate != null) { @@ -1056,6 +1098,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); } } + } catch (Exception e) { + log.warn("Error updating trade period state for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), e.getMessage(), e); + continue; } } } @@ -1069,12 +1114,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // we move the trade to FailedTradesManager public void onMoveInvalidTradeToFailedTrades(Trade trade) { failedTradesManager.add(trade); - removeTrade(trade, false); + removeTrade(trade); + xmrWalletService.fixReservedOutputs(); } public void onMoveFailedTradeToPendingTrades(Trade trade) { addTradeToPendingTrades(trade); failedTradesManager.removeTrade(trade); + xmrWalletService.fixReservedOutputs(); } public void onMoveClosedTradeToPendingTrades(Trade trade) { @@ -1090,7 +1137,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void addTradeToPendingTrades(Trade trade) { if (!trade.isInitialized()) { try { - initPersistedTrade(trade); + initTrade(trade); } catch (Exception e) { log.warn("Error initializing {} {} on move to pending trades", trade.getClass().getSimpleName(), trade.getShortId(), e); } @@ -1173,7 +1220,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return false; } - initPersistedTrade(trade); + initTrade(trade); UserThread.execute(() -> { synchronized (tradableList.getList()) { @@ -1204,7 +1251,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // Getters, Utils /////////////////////////////////////////////////////////////////////////////////////////// - public void sendAckMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, TradeMessage message, boolean result, @Nullable String errorMessage) { + public void sendAckMessage(NodeAddress peer, PubKeyRing peersPubKeyRing, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // create ack message String tradeId = message.getOfferId(); @@ -1215,11 +1262,21 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi sourceUid, tradeId, result, - errorMessage); + errorMessage, + updatedMultisigHex); // send ack message - log.info("Send AckMessage for {} to peer {}. tradeId={}, sourceUid={}", - ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + if (!result) { + if (errorMessage == null) { + log.warn("Sending NACK for {} to peer {} without error message. That should never happen. tradeId={}, sourceUid={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + } + log.warn("Sending NACK for {} to peer {}. tradeId={}, sourceUid={}, errorMessage={}, updatedMultisigHex={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid, errorMessage, updatedMultisigHex == null ? "null" : updatedMultisigHex.length() + " characters"); + } else { + log.info("Sending AckMessage for {} to peer {}. tradeId={}, sourceUid={}", + ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid); + } p2PService.getMailboxMessageService().sendEncryptedMailboxMessage( peer, peersPubKeyRing, @@ -1252,8 +1309,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } - public BooleanProperty persistedTradesInitializedProperty() { - return persistedTradesInitialized; + public BooleanProperty tradesInitializedProperty() { + return tradesInitialized; } public boolean isMyOffer(Offer offer) { @@ -1274,11 +1331,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return offer.getDirection() == OfferDirection.SELL; } - // TODO (woodser): make Optional versus Trade return types consistent + // TODO: make Optional versus Trade return types consistent public Trade getTrade(String tradeId) { return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null))); } + public boolean hasTrade(String tradeId) { + return getTrade(tradeId) != null; + } + public Optional getOpenTrade(String tradeId) { synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); @@ -1345,6 +1406,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // TODO Remove once tradableList is refactored to a final field // (part of the persistence refactor PR) private void onTradesChanged() { - this.numPendingTrades.set(getObservableList().size()); + synchronized (numPendingTrades) { + numPendingTrades.set(getObservableList().size()); + } } } diff --git a/core/src/main/java/haveno/core/trade/TradeUtil.java b/core/src/main/java/haveno/core/trade/TradeUtil.java index bfebca486b..186e5410a2 100644 --- a/core/src/main/java/haveno/core/trade/TradeUtil.java +++ b/core/src/main/java/haveno/core/trade/TradeUtil.java @@ -23,7 +23,6 @@ import com.google.inject.Singleton; import haveno.common.crypto.KeyRing; import haveno.common.util.Tuple2; import static haveno.core.locale.CurrencyUtil.getCurrencyPair; -import static haveno.core.locale.CurrencyUtil.isTraditionalCurrency; import haveno.core.locale.Res; import haveno.core.offer.Offer; import static haveno.core.util.FormattingUtils.formatDurationAsWords; @@ -153,8 +152,8 @@ public class TradeUtil { return ""; checkNotNull(trade.getOffer()); - checkNotNull(trade.getOffer().getCurrencyCode()); - return getCurrencyPair(trade.getOffer().getCurrencyCode()); + checkNotNull(trade.getOffer().getCounterCurrencyCode()); + return getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); } public String getPaymentMethodNameWithCountryCode(Trade trade) { @@ -180,7 +179,7 @@ public class TradeUtil { return (trade.isArbitrator() ? "Arbitrator for " : "") + // TODO: use Res.get() getRole(trade.getBuyer() == trade.getMaker(), trade.isArbitrator() ? true : trade.isMaker(), // arbitrator role in context of maker - offer.getCurrencyCode()); + offer.getCounterCurrencyCode()); } /** @@ -192,25 +191,14 @@ public class TradeUtil { * @return String describing a trader's role */ private static String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { - if (isTraditionalCurrency(currencyCode)) { - String baseCurrencyCode = Res.getBaseCurrencyCode(); - if (isBuyerMakerAndSellerTaker) - return isMaker - ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.buyer")) - : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.seller")); - else - return isMaker - ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.seller")) - : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.buyer")); - } else { - if (isBuyerMakerAndSellerTaker) - return isMaker - ? Res.get("formatter.asMaker", currencyCode, Res.get("shared.seller")) - : Res.get("formatter.asTaker", currencyCode, Res.get("shared.buyer")); - else - return isMaker - ? Res.get("formatter.asMaker", currencyCode, Res.get("shared.buyer")) - : Res.get("formatter.asTaker", currencyCode, Res.get("shared.seller")); - } + String baseCurrencyCode = Res.getBaseCurrencyCode(); + if (isBuyerMakerAndSellerTaker) + return isMaker + ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.buyer")) + : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.seller")); + else + return isMaker + ? Res.get("formatter.asMaker", baseCurrencyCode, Res.get("shared.seller")) + : Res.get("formatter.asTaker", baseCurrencyCode, Res.get("shared.buyer")); } } diff --git a/core/src/main/java/haveno/core/trade/messages/PaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/messages/PaymentReceivedMessage.java index a7f23190d6..8909fcf226 100644 --- a/core/src/main/java/haveno/core/trade/messages/PaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/messages/PaymentReceivedMessage.java @@ -51,6 +51,9 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { @Setter @Nullable private byte[] sellerSignature; + @Setter + @Nullable + private String payoutTxId; public PaymentReceivedMessage(String tradeId, NodeAddress senderNodeAddress, @@ -61,7 +64,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { boolean deferPublishPayout, AccountAgeWitness buyerAccountAgeWitness, @Nullable SignedWitness buyerSignedWitness, - @Nullable PaymentSentMessage paymentSentMessage) { + @Nullable PaymentSentMessage paymentSentMessage, + @Nullable String payoutTxId) { this(tradeId, senderNodeAddress, uid, @@ -72,7 +76,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { deferPublishPayout, buyerAccountAgeWitness, buyerSignedWitness, - paymentSentMessage); + paymentSentMessage, + payoutTxId); } @@ -90,7 +95,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { boolean deferPublishPayout, AccountAgeWitness buyerAccountAgeWitness, @Nullable SignedWitness buyerSignedWitness, - PaymentSentMessage paymentSentMessage) { + PaymentSentMessage paymentSentMessage, + @Nullable String payoutTxId) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.unsignedPayoutTxHex = unsignedPayoutTxHex; @@ -100,6 +106,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { this.paymentSentMessage = paymentSentMessage; this.buyerAccountAgeWitness = buyerAccountAgeWitness; this.buyerSignedWitness = buyerSignedWitness; + this.payoutTxId = payoutTxId; } @Override @@ -116,6 +123,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { Optional.ofNullable(buyerSignedWitness).ifPresent(buyerSignedWitness -> builder.setBuyerSignedWitness(buyerSignedWitness.toProtoSignedWitness())); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e))); + Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); } @@ -138,7 +146,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { proto.getDeferPublishPayout(), buyerAccountAgeWitness, buyerSignedWitness, - proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); + proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null, + ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature())); return message; } @@ -154,6 +163,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { ",\n deferPublishPayout=" + deferPublishPayout + ",\n paymentSentMessage=" + paymentSentMessage + ",\n sellerSignature=" + sellerSignature + + ",\n payoutTxId=" + payoutTxId + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index 4302f6db6f..348f629410 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -120,13 +120,23 @@ public class BuyerProtocol extends DisputeProtocol { public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); + + // advance trade state + if (trade.isDepositsUnlocked() || trade.isDepositsFinalized() || trade.isPaymentSent()) { + trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT); + } else { + errorMessageHandler.handleErrorMessage("Cannot confirm payment sent for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState()); + return; + } + + // process on trade thread ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; BuyerEvent event = BuyerEvent.PAYMENT_SENT; try { - expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT) + expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks(ApplyFilter.class, @@ -145,7 +155,6 @@ public class BuyerProtocol extends DisputeProtocol { trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); handleTaskRunnerFault(event, errorMessage); }))) - .run(() -> trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT)) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage()); diff --git a/core/src/main/java/haveno/core/trade/protocol/DisputeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/DisputeProtocol.java index 75715559be..fbb40ee1c0 100644 --- a/core/src/main/java/haveno/core/trade/protocol/DisputeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/DisputeProtocol.java @@ -80,7 +80,9 @@ public abstract class DisputeProtocol extends TradeProtocol { // Trader has not yet received the peer's signature but has clicked the accept button. public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; - expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, + expect(anyPhase( + Trade.Phase.DEPOSITS_UNLOCKED, + Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) @@ -107,7 +109,9 @@ public abstract class DisputeProtocol extends TradeProtocol { // Trader has already received the peer's signature and has clicked the accept button as well. public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; - expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, + expect(anyPhase( + Trade.Phase.DEPOSITS_UNLOCKED, + Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(event) @@ -135,7 +139,9 @@ public abstract class DisputeProtocol extends TradeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) { - expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, + expect(anyPhase( + Trade.Phase.DEPOSITS_UNLOCKED, + Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(message) @@ -145,7 +151,9 @@ public abstract class DisputeProtocol extends TradeProtocol { } protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) { - expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, + expect(anyPhase( + Trade.Phase.DEPOSITS_UNLOCKED, + Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(message) diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index ae6a27e7f0..91d23d40f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -166,6 +166,9 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private boolean importMultisigHexScheduled; + @Getter + @Setter + private boolean paymentSentPayoutTxStale; private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); @Deprecated private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); @@ -237,7 +240,8 @@ public class ProcessModel implements Model, PersistablePayload { .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) .setTradeProtocolErrorHeight(tradeProtocolErrorHeight) - .setImportMultisigHexScheduled(importMultisigHexScheduled); + .setImportMultisigHexScheduled(importMultisigHexScheduled) + .setPaymentSentPayoutTxStale(paymentSentPayoutTxStale); Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage())); Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage())); Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage())); @@ -262,6 +266,7 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight()); processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled()); + processModel.setPaymentSentPayoutTxStale(proto.getPaymentSentPayoutTxStale()); // nullable processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); @@ -311,7 +316,7 @@ public class ProcessModel implements Model, PersistablePayload { void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setDepositTxMessageState(messageState); } @@ -326,6 +331,17 @@ public class ProcessModel implements Model, PersistablePayload { getAccountAgeWitnessService().getAccountAgeWitnessUtils().witnessDebugLog(trade, null); } + public boolean maybeClearSensitiveData() { + boolean changed = false; + for (TradePeer tradingPeer : getTradePeers()) { + if (tradingPeer.getPaymentAccountPayload() != null || tradingPeer.getContractAsJson() != null) { + tradingPeer.setPaymentAccountPayload(null); + tradingPeer.setContractAsJson(null); + changed = true; + } + } + return changed; + } /////////////////////////////////////////////////////////////////////////////////////////// // Delegates diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 2a01c5a2ca..2d7479a777 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -68,15 +68,16 @@ public class SellerProtocol extends DisputeProtocol { protected void onInitialized() { super.onInitialized(); - // re-send payment received message if payout not published + // re-send payment received message if not acked ThreadUtils.execute(() -> { - if (!needsToResendPaymentReceivedMessages()) return; + if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return; synchronized (trade.getLock()) { - if (!needsToResendPaymentReceivedMessages()) return; + if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return; latchTrade(); given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) .with(SellerEvent.STARTUP)) .setup(tasks( + SellerPreparePaymentReceivedMessage.class, SellerSendPaymentReceivedMessageToBuyer.class, SellerSendPaymentReceivedMessageToArbitrator.class) .using(new TradeTaskRunner(trade, @@ -93,13 +94,6 @@ public class SellerProtocol extends DisputeProtocol { }, trade.getId()); } - public boolean needsToResendPaymentReceivedMessages() { - return !trade.isShutDownStarted() && trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.getProcessModel().isPaymentReceivedMessagesReceived() && resendPaymentReceivedMessagesEnabled(); - } - - private boolean resendPaymentReceivedMessagesEnabled() { - return trade.getOffer().getOfferPayload().getProtocolVersion() >= 2; - } @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { @@ -123,6 +117,16 @@ public class SellerProtocol extends DisputeProtocol { public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); + + // advance trade state + if (trade.isPaymentSent() || trade.isPaymentReceived()) { + trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + } else { + errorMessageHandler.handleErrorMessage("Cannot confirm payment received for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState()); + return; + } + + // process on trade thread ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -144,10 +148,9 @@ public class SellerProtocol extends DisputeProtocol { resultHandler.handleResult(); }, (errorMessage) -> { log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage); - trade.resetToPaymentSentState(); + if (!trade.isPayoutPublished()) trade.resetToPaymentSentState(); handleTaskRunnerFault(event, errorMessage); }))) - .run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage()); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index 11c035a329..3116746dd6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -208,21 +208,21 @@ public final class TradePeer implements PersistablePayload { void setDepositsConfirmedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setDepositsConfirmedMessageState(messageState); } void setPaymentSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setPaymentSentMessageState(messageState); } void setPaymentReceivedAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setPaymentReceivedMessageState(messageState); } @@ -259,6 +259,10 @@ public final class TradePeer implements PersistablePayload { return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; } + public boolean isPaymentReceivedMessageArrived() { + return paymentReceivedMessageStateProperty.get() == MessageState.ARRIVED; + } + @Override public Message toProtoMessage() { final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index a1ad25ddaf..b4c809a20e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -42,6 +42,8 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.Task; +import haveno.core.offer.OpenOffer; +import haveno.core.support.messages.ChatMessage; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -54,13 +56,17 @@ import haveno.core.trade.messages.DepositRequest; import haveno.core.trade.messages.DepositResponse; import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.InitMultisigRequest; +import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractResponse; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.FluentProtocol.Condition; +import haveno.core.trade.protocol.FluentProtocol.Event; import haveno.core.trade.protocol.tasks.ApplyFilter; +import haveno.core.trade.protocol.tasks.MakerRecreateReserveTx; +import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.MaybeSendSignContractRequest; import haveno.core.trade.protocol.tasks.ProcessDepositResponse; import haveno.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; @@ -94,9 +100,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 60 : 180; private static final String TIMEOUT_REACHED = "Timeout reached."; - public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions + public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other protocol functions + public static final int REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS = 2; // request connection switch on even attempts public static final long REPROCESS_DELAY_MS = 5000; public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files + public static final String SEND_INIT_TRADE_REQUEST_FAILED = "Sending InitTradeRequest failed"; protected final ProcessModel processModel; protected final Trade trade; @@ -109,6 +117,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private boolean depositsConfirmedTasksCalled; private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; + private boolean makerInitTradeRequestHasBeenNacked = false; + private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null; + + private static int MAX_PAYMENT_RECEIVED_NACKS = 5; + private int numPaymentReceivedNacks = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -249,7 +262,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); // reprocess applicable messages - trade.reprocessApplicableMessages(); + trade.initializeAfterMailboxMessages(); } // send deposits confirmed message if applicable @@ -272,7 +285,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerSuccess(null, null, "maybeSendDepositsConfirmedMessages"); }, (errorMessage) -> { - handleTaskRunnerFault(null, null, "maybeSendDepositsConfirmedMessages", errorMessage); + handleTaskRunnerFault(null, null, "maybeSendDepositsConfirmedMessages", errorMessage, null); }))) .executeTasks(true); awaitTradeLatch(); @@ -280,10 +293,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } - public boolean needsToResendPaymentReceivedMessages() { - return false; // seller protocol overrides - } - public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { @@ -460,9 +469,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D .using(new TradeTaskRunner(trade, () -> { stopTimeout(); - this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED - handleTaskRunnerSuccess(sender, response); - if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized + + // tasks may complete successfully but process an error + if (trade.getInitError() == null) { + this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED + handleTaskRunnerSuccess(sender, response); + if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized + } else { + handleTaskRunnerSuccess(sender, response); + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(trade.getInitError().getMessage()); + } + + this.tradeResultHandler = null; + this.errorMessageHandler = null; }, errorMessage -> { handleTaskRunnerFault(sender, response, errorMessage); @@ -517,6 +536,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } + // log warning if trade not open + if (!processModel.getTradeManager().hasOpenTrade(trade)) { + log.warn("We received a PaymentSentMessage for {} {} but it is not an open trade. This can happen if the trade is pending processing as a failed trade.", trade.getClass().getSimpleName(), trade.getId()); + } + // validate signature try { HavenoUtils.verifyPaymentSentMessage(trade, message); @@ -527,62 +551,67 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // save message for reprocessing trade.getBuyer().setPaymentSentMessage(message); - trade.requestPersistence(); + trade.persistNow(() -> { - // process message on trade thread - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - ThreadUtils.execute(() -> { - // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case - // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received - // a mailbox message with PaymentSentMessage. - // TODO A better fix would be to add a listener for the wallet sync state and process - // the mailbox msg once wallet is ready and trade state set. - synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); - handleTaskRunnerSuccess(peer, message); - return; + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case + // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received + // a mailbox message with PaymentSentMessage. + // TODO A better fix would be to add a listener for the wallet sync state and process + // the mailbox msg once wallet is ready and trade state set. + synchronized (trade.getLock()) { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { + log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); + handleTaskRunnerSuccess(peer, message); + return; + } + if (trade.getPayoutTx() != null) { + log.warn("We received a PaymentSentMessage but we have already created the payout tx " + + "so we ignore the message. This can happen if the ACK message to the peer did not " + + "arrive and the peer repeats sending us the message. We send another ACK msg."); + sendAckMessage(peer, message, true, null); + removeMailboxMessageAfterProcessing(message); + return; + } + if (message != trade.getBuyer().getPaymentSentMessage()) { + log.warn("Ignoring PaymentSentMessage which was replaced by a newer message", trade.getClass().getSimpleName(), trade.getId()); + return; + } + latchTrade(); + expect(anyPhase() + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessPaymentSentMessage.class, + VerifyPeersAccountAgeWitness.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(peer, message); + }, + (errorMessage) -> { + log.warn("Error processing payment sent message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message unless deleted + if (trade.getBuyer().getPaymentSentMessage() != null) { + UserThread.runAfter(() -> { + reprocessPaymentSentMessageCount++; + maybeReprocessPaymentSentMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); + } else { + handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + } + unlatchTrade(); + }))) + .executeTasks(true); + awaitTradeLatch(); } - if (trade.getPayoutTx() != null) { - log.warn("We received a PaymentSentMessage but we have already created the payout tx " + - "so we ignore the message. This can happen if the ACK message to the peer did not " + - "arrive and the peer repeats sending us the message. We send another ACK msg."); - sendAckMessage(peer, message, true, null); - removeMailboxMessageAfterProcessing(message); - return; - } - latchTrade(); - expect(anyPhase() - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessPaymentSentMessage.class, - VerifyPeersAccountAgeWitness.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - (errorMessage) -> { - log.warn("Error processing payment sent message: " + errorMessage); - processModel.getTradeManager().requestPersistence(); - - // schedule to reprocess message unless deleted - if (trade.getBuyer().getPaymentSentMessage() != null) { - UserThread.runAfter(() -> { - reprocessPaymentSentMessageCount++; - maybeReprocessPaymentSentMessage(reprocessOnError); - }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); - } else { - handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack - } - unlatchTrade(); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + }, trade.getId()); + }); } // received by buyer and arbitrator @@ -609,59 +638,88 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // save message for reprocessing trade.getSeller().setPaymentReceivedMessage(message); - trade.requestPersistence(); - // process message on trade thread - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - ThreadUtils.execute(() -> { - synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); + // persist trade before processing on trade thread + trade.persistNow(() -> { - // check minimum trade phase - if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + if (!trade.isInitialized() || trade.isShutDownStarted()) { + log.warn("Skipping processing PaymentReceivedMessage because the trade is not initialized or it's shutting down for {} {}", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) { + log.warn("Received another PaymentReceivedMessage after payout is published {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); + handleTaskRunnerSuccess(peer, message); + return; + } + if (message != trade.getSeller().getPaymentReceivedMessage()) { + log.warn("Ignoring PaymentReceivedMessage which was replaced by a newer message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (lastAckedPaymentReceivedMessage != null && lastAckedPaymentReceivedMessage.equals(trade.getSeller().getPaymentReceivedMessage())) { + log.warn("Ignoring PaymentReceivedMessage which was already processed and responded to for {} {}", trade.getClass().getSimpleName(), trade.getId()); + return; + } + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + + // check minimum trade phase + if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) { + log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { + log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { + log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); + return; + } + + expect(anyPhase() + .with(message) + .from(peer)) + .setup(tasks( + ProcessPaymentReceivedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + lastAckedPaymentReceivedMessage = message; + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + log.warn("Error processing payment received message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message or nack + if (trade.getSeller().getPaymentReceivedMessage() != null) { + if (reprocessOnError) { + UserThread.runAfter(() -> { + reprocessPaymentReceivedMessageCount++; + maybeReprocessPaymentReceivedMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); + } + unlatchTrade(); + } else { + + // export fresh multisig info for nack + trade.exportMultisigHex(); + + // handle payout error + lastAckedPaymentReceivedMessage = message; + trade.onPayoutError(false, false, null); + handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // send nack + } + }))) + .executeTasks(true); + awaitTradeLatch(); } - if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { - log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; - } - if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { - log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId()); - return; - } - - expect(anyPhase() - .with(message) - .from(peer)) - .setup(tasks( - ProcessPaymentReceivedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - log.warn("Error processing payment received message: " + errorMessage); - processModel.getTradeManager().requestPersistence(); - - // schedule to reprocess message unless deleted - if (trade.getSeller().getPaymentReceivedMessage() != null) { - UserThread.runAfter(() -> { - reprocessPaymentReceivedMessageCount++; - maybeReprocessPaymentReceivedMessage(reprocessOnError); - }, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount)); - } else { - handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack - } - unlatchTrade(); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + }, trade.getId()); + }); } public void onWithdrawCompleted() { @@ -682,7 +740,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerFault(null, null, result.name(), - result.getInfo()); + result.getInfo(), + null); } }); } @@ -720,9 +779,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D /////////////////////////////////////////////////////////////////////////////////////////// private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + boolean processOnTradeThread = !ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName()); // handle chat message acks off trade thread for responsiveness if the thread is busy + if (processOnTradeThread) { + ThreadUtils.execute(() -> onAckMessageAux(ackMessage, sender), trade.getId()); + } else { + onAckMessageAux(ackMessage, sender); + } + } + // TODO: this has grown in complexity over time and could use refactoring + private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) { + // ignore if trade is completely finished - if (trade.isFinished()) return; + if (trade.isFinished()) return; // get trade peer TradePeer peer = trade.getTradePeer(sender); @@ -743,7 +812,24 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D peer.setNodeAddress(sender); } - // set trade state on deposit request nack + // handle nack of InitTradeRequest from arbitrator to maker + if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) { + if (ignoreInitTradeRequestNackFromArbitrator(ackMessage)) { + log.warn("Ignoring InitTradeRequest NACK from arbitrator, offerId={}, errorMessage={}", processModel.getOfferId(), ackMessage.getErrorMessage()); + // use default postprocessing + } else { + if (makerInitTradeRequestHasBeenNacked) { + handleSecondMakerInitTradeRequestNack(ackMessage); + // use default postprocessing + } else { + makerInitTradeRequestHasBeenNacked = true; + handleFirstMakerInitTradeRequestNack(ackMessage); + return; + } + } + } + + // handle nack of deposit request if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); @@ -751,42 +837,96 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { peer.setDepositsConfirmedAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } - // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { - if (trade.getTradePeer(sender) == trade.getSeller()) { + if (!trade.isPaymentMarkedSent()) { + log.warn("Received AckMessage for PaymentSentMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); + return; + } + if (peer == trade.getSeller()) { trade.getSeller().setPaymentSentAckMessage(ackMessage); if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + } else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentSentAckMessage(ackMessage); processModel.getTradeManager().requestPersistence(); } else { - log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } } - // handle ack for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + // handle ack message for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + // TODO: trade state can be reset twice if both peers nack before published payout is detected + // TODO: do not reset state if payment received message is acknowledged because payout is likely broadcast? if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { - if (trade.getTradePeer(sender) == trade.getBuyer()) { + + // ack message from buyer + if (peer == trade.getBuyer()) { trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); - if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); - else trade.setState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); - processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + processModel.getTradeManager().persistNow(null); + + // handle successful ack + if (ackMessage.isSuccess()) { + + // validate state + if (!trade.isPaymentMarkedReceived()) { + log.warn("Received AckMessage for PaymentReceivedMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); + return; + } + + trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); + processModel.getTradeManager().persistNow(null); + } + + // handle nack + else { + log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage()); + + // nack includes updated multisig hex since v1.1.1 + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + processModel.getTradeManager().persistNow(null); + boolean autoResent = onPaymentReceivedNack(true, peer); + if (autoResent) return; // skip remaining processing if auto resent + } + } + } + + // ack message from arbitrator + else if (peer == trade.getArbitrator()) { trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); - processModel.getTradeManager().requestPersistence(); + processModel.getTradeManager().persistNow(null); + + // handle nack + if (!ackMessage.isSuccess()) { + log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage()); + + // nack includes updated multisig hex since v1.1.1 + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + processModel.getTradeManager().persistNow(null); + boolean autoResent = onPaymentReceivedNack(true, peer); + if (autoResent) return; // skip remaining processing if auto resent + } + } } else { - log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid()); return; } + + // clear and shut down trade if completely finished after ack + if (trade.isFinished()) { + log.info("Trade {} {} is finished after PaymentReceivedMessage ACK, shutting it down", trade.getClass().getSimpleName(), trade.getId()); + trade.clearAndShutDown(); + } } // generic handling @@ -794,14 +934,79 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); } else { log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); - handleError(ackMessage.getErrorMessage()); + handleError("Your peer had a problem processing your message. Please ensure you and your peer are running the latest version and try again.\n\nError details:\n" + ackMessage.getErrorMessage()); } // notify trade listeners trade.onAckMessage(ackMessage, sender); } + private static boolean ignoreInitTradeRequestNackFromArbitrator(AckMessage ackMessage) { + return ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED); // ignore if arbitrator's request failed to taker + } + + private boolean onPaymentReceivedNack(boolean syncAndPoll, TradePeer peer) { + + // prevent infinite nack loop with max attempts + numPaymentReceivedNacks++; + if (numPaymentReceivedNacks > MAX_PAYMENT_RECEIVED_NACKS) { + String errorMsg = "The maximum number of attempts to process the payment confirmation has been reached for " + trade.getClass().getSimpleName() + " " + trade.getId() + ". Restart the application to try again."; + log.warn(errorMsg); + trade.setErrorMessage(errorMsg); + return false; + } + + // handle payout error + return trade.onPayoutError(syncAndPoll, true, peer); + } + + private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) { + log.warn("Maker received NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + ThreadUtils.execute(() -> { + Event event = new Event() { + @Override + public String name() { + return "MakerRecreateReserveTx"; + } + }; + synchronized (trade.getLock()) { + latchTrade(); + expect(phase(Trade.Phase.INIT) + .with(event)) + .setup(tasks( + MakerRecreateReserveTx.class, + MakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + unlatchTrade(); + }, + errorMessage -> { + handleError("Failed to re-send InitTradeRequest to arbitrator for " + trade.getClass().getSimpleName() + " " + trade.getId() + ": " + errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); + } + + private void handleSecondMakerInitTradeRequestNack(AckMessage ackMessage) { + log.warn("Maker received 2nd NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + String warningMessage = "Your offer (" + trade.getOffer().getShortId() + ") has been removed because there was a problem taking the trade.\n\nError message: " + ackMessage.getErrorMessage(); + OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getId()).orElse(null); + if (openOffer != null) { + HavenoUtils.openOfferManager.cancelOpenOffer(openOffer, null, null); + HavenoUtils.setTopError(warningMessage); + } + log.warn(warningMessage); + } + protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) { + sendAckMessage(peer, message, result, errorMessage, null); + } + + protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage, String updatedMultisigHex) { // get peer's pub key ring PubKeyRing peersPubKeyRing = getPeersPubKeyRing(peer); @@ -811,7 +1016,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } // send ack message - processModel.getTradeManager().sendAckMessage(peer, peersPubKeyRing, message, result, errorMessage); + processModel.getTradeManager().sendAckMessage(peer, peersPubKeyRing, message, result, errorMessage, updatedMultisigHex); } @@ -832,7 +1037,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - protected synchronized void stopTimeout() { + public synchronized void stopTimeout() { synchronized (timeoutTimerLock) { if (timeoutTimer != null) { timeoutTimer.stop(); @@ -858,11 +1063,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } protected void handleTaskRunnerFault(NodeAddress sender, TradeMessage message, String errorMessage) { - handleTaskRunnerFault(sender, message, message.getClass().getSimpleName(), errorMessage); + handleTaskRunnerFault(sender, message, message.getClass().getSimpleName(), errorMessage, null); } protected void handleTaskRunnerFault(FluentProtocol.Event event, String errorMessage) { - handleTaskRunnerFault(null, null, event.name(), errorMessage); + handleTaskRunnerFault(null, null, event.name(), errorMessage, null); } @@ -924,14 +1129,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D unlatchTrade(); } - void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) { + void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex) { log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection()); - if (message != null) { - sendAckMessage(ackReceiver, message, false, errorMessage); - } - handleError(errorMessage); + + if (message != null) { + sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex); + } } // these are not thread safe, so they must be used within a lock on the trade @@ -941,9 +1146,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D log.error(errorMessage); trade.setErrorMessage(errorMessage); processModel.getTradeManager().requestPersistence(); + unlatchTrade(); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler = null; - unlatchTrade(); } protected void latchTrade() { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ApplyFilter.java index 686f17f361..d2c08da515 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ApplyFilter.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ApplyFilter.java @@ -45,9 +45,9 @@ public class ApplyFilter extends TradeTask { } else if (filterManager.isOfferIdBanned(trade.getId())) { failed("Offer ID is banned.\n" + "Offer ID=" + trade.getId()); - } else if (trade.getOffer() != null && filterManager.isCurrencyBanned(trade.getOffer().getCurrencyCode())) { + } else if (trade.getOffer() != null && filterManager.isCurrencyBanned(trade.getOffer().getCounterCurrencyCode())) { failed("Currency is banned.\n" + - "Currency code=" + trade.getOffer().getCurrencyCode()); + "Currency code=" + trade.getOffer().getCounterCurrencyCode()); } else if (filterManager.isPaymentMethodBanned(checkNotNull(trade.getOffer()).getPaymentMethod())) { failed("Payment method is banned.\n" + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 3a7fc7ace9..026604fc75 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -95,41 +95,59 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // set peer's signature sender.setContractSignature(signature); + // subscribe to trade state once to send responses with ack or nack + if (!hasBothContractSignatures()) { + trade.stateProperty().addListener((obs, oldState, newState) -> { + if (oldState == newState) return; + if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { + sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); + } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { + sendDepositResponsesOnce(null); + } + }); + } + // collect expected values Offer offer = trade.getOffer(); boolean isFromTaker = sender == trade.getTaker(); boolean isFromBuyer = sender == trade.getBuyer(); BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee(); BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount(); - BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + BigInteger securityDepositBeforeMiningFee = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); String depositAddress = processModel.getMultisigAddress(); - sender.setSecurityDeposit(securityDeposit); + sender.setSecurityDeposit(securityDepositBeforeMiningFee); // verify deposit tx boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit(); if (!isFromBuyerAsTakerWithoutDeposit) { - MoneroTx verifiedTx; try { - verifiedTx = trade.getXmrWalletService().verifyDepositTx( + MoneroTx verifiedTx = trade.getXmrWalletService().verifyDepositTx( offer.getId(), tradeFee, trade.getProcessModel().getTradeFeeAddress(), sendTradeAmount, - securityDeposit, + securityDepositBeforeMiningFee, depositAddress, sender.getDepositTxHash(), request.getDepositTxHex(), request.getDepositTxKey(), null); + + // TODO: it seems a deposit tx had 0 fee once? + if (BigInteger.ZERO.equals(verifiedTx.getFee())) { + String errorMessage = "Deposit transaction from " + (isFromTaker ? "taker" : "maker") + " has 0 fee for trade " + trade.getId() + ". This should never happen."; + log.warn(errorMessage + "\n" + verifiedTx); + throw new RuntimeException(errorMessage); + } + + // update trade state + sender.setSecurityDeposit(securityDepositBeforeMiningFee.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setDepositTxFee(verifiedTx.getFee()); + sender.setDepositTxHex(request.getDepositTxHex()); + sender.setDepositTxKey(request.getDepositTxKey()); } catch (Exception e) { throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); } - - // update trade state - sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - sender.setDepositTxFee(verifiedTx.getFee()); - sender.setDepositTxHex(request.getDepositTxHex()); - sender.setDepositTxKey(request.getDepositTxKey()); } // update trade state @@ -137,8 +155,8 @@ public class ArbitratorProcessDepositRequest extends TradeTask { processModel.getTradeManager().requestPersistence(); // relay deposit txs when both requests received - MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { + MoneroDaemon monerod = trade.getXmrWalletService().getMonerod(); + if (hasBothContractSignatures()) { // check timeout and extend just before relaying if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); @@ -150,19 +168,23 @@ public class ArbitratorProcessDepositRequest extends TradeTask { try { // submit maker tx to pool but do not relay - MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true); + MoneroSubmitTxResult makerResult = monerod.submitTxHex(processModel.getMaker().getDepositTxHex(), true); if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult)); txHashes.add(processModel.getMaker().getDepositTxHash()); // submit taker tx to pool but do not relay if (!trade.hasBuyerAsTakerWithoutDeposit()) { - MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); + MoneroSubmitTxResult takerResult = monerod.submitTxHex(processModel.getTaker().getDepositTxHex(), true); if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); txHashes.add(processModel.getTaker().getDepositTxHash()); } // relay txs - daemon.relayTxsByHash(txHashes); + try { + monerod.relayTxsByHash(txHashes); // call will error if txs are already confirmed, but they're still relayed + } catch (Exception e) { + log.warn("Error relaying deposit txs for trade {}. They could already be confirmed. Error={}", trade.getId(), e.getMessage()); + } depositTxsRelayed = true; // update trade state @@ -174,7 +196,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // flush txs from pool try { - daemon.flushTxPool(txHashes); + monerod.flushTxPool(txHashes); } catch (Exception e2) { log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2); } @@ -182,22 +204,15 @@ public class ArbitratorProcessDepositRequest extends TradeTask { throw e; } } else { - - // subscribe to trade state once to send responses with ack or nack - trade.stateProperty().addListener((obs, oldState, newState) -> { - if (oldState == newState) return; - if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { - sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); - } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { - sendDepositResponsesOnce(null); - } - }); - if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); } } + private boolean hasBothContractSignatures() { + return processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null; + } + private boolean isTimedOut() { return !processModel.getTradeManager().hasOpenTrade(trade); } @@ -210,7 +225,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // log error if (errorMessage != null) { - log.warn("Sending deposit responses with error={}", errorMessage, new Throwable("Stack trace")); + log.warn("Sending deposit responses for tradeId={}, error={}", trade.getId(), errorMessage); } // create deposit response @@ -229,7 +244,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { } private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { - log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error); + log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), response.getErrorMessage()); processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { @Override public void onArrived() { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index 18e97dd466..51363452ea 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -64,12 +64,11 @@ public class ArbitratorProcessReserveTx extends TradeTask { if (!isFromBuyerAsTakerWithoutDeposit) { // process reserve tx with expected values - BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); + BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee(); BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount - MoneroTx verifiedTx; try { - verifiedTx = trade.getXmrWalletService().verifyReserveTx( + MoneroTx verifiedTx = trade.getXmrWalletService().verifyReserveTx( offer.getId(), penaltyFee, tradeFee, @@ -80,16 +79,23 @@ public class ArbitratorProcessReserveTx extends TradeTask { request.getReserveTxHex(), request.getReserveTxKey(), null); + + // TODO: it seems a deposit tx had 0 fee once? + if (BigInteger.ZERO.equals(verifiedTx.getFee())) { + String errorMessage = "Reserve transaction from " + (isFromMaker ? "maker" : "taker") + " has 0 fee for trade " + trade.getId() + ". This should never happen."; + log.warn(errorMessage + "\n" + verifiedTx); + throw new RuntimeException(errorMessage); + } + + // save reserve tx to model + sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit + sender.setReserveTxHash(request.getReserveTxHash()); + sender.setReserveTxHex(request.getReserveTxHex()); + sender.setReserveTxKey(request.getReserveTxKey()); } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); } - - // save reserve tx to model - sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - sender.setReserveTxHash(request.getReserveTxHash()); - sender.setReserveTxHex(request.getReserveTxHex()); - sender.setReserveTxKey(request.getReserveTxKey()); } // persist trade diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index a846077db4..1939523cc5 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -25,6 +25,7 @@ import haveno.core.trade.Trade; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.TradePeer; +import haveno.core.trade.protocol.TradeProtocol; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; @@ -71,7 +72,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { UUID.randomUUID().toString(), Version.getP2PMessageVersion(), request.getAccountAgeWitnessSignatureOfOfferId(), - new Date().getTime(), + request.getCurrentDate(), trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), @@ -95,8 +96,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { } @Override public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", takerRequest.getClass().getSimpleName(), takerRequest.getUid(), trade.getTaker().getNodeAddress(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + takerRequest + "\nerrorMessage=" + errorMessage); + appendToErrorMessage(TradeProtocol.SEND_INIT_TRADE_REQUEST_FAILED + ": errorMessage=" + errorMessage); failed(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index 05fee1374a..ad40f761e9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -83,16 +83,21 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { // synchronize on lock for wallet operations synchronized (trade.getWalletLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) { + try { - // import multisig hex - trade.importMultisigHex(); + // import multisig hex + trade.importMultisigHex(); - // create payout tx - log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); - MoneroTxWallet payoutTx = trade.createPayoutTx(); - trade.updatePayout(payoutTx); - trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - trade.requestPersistence(); + // create payout tx + log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); + MoneroTxWallet payoutTx = trade.createPayoutTx(); + trade.setPayoutTx(payoutTx); + trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + trade.requestPersistence(); + } catch (Exception e) { + if (HavenoUtils.isIllegal(e)) log.warn("Failed to create unsigned payout tx for " + trade.getClass().getSimpleName() + " " + trade.getShortId(), e); // continue to send message if illegal state + else throw e; + } } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java new file mode 100644 index 0000000000..cee6e0d9c6 --- /dev/null +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java @@ -0,0 +1,147 @@ +/* + * 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 . + */ + +package haveno.core.trade.protocol.tasks; + +import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.MakerTrade; +import haveno.core.trade.Trade; +import haveno.core.trade.protocol.TradeProtocol; +import haveno.core.xmr.model.XmrAddressEntry; +import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; +import monero.wallet.model.MoneroTxWallet; + +import java.math.BigInteger; + +@Slf4j +public class MakerRecreateReserveTx extends TradeTask { + + public MakerRecreateReserveTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // maker trade expected + if (!(trade instanceof MakerTrade)) { + throw new RuntimeException("Expected maker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); + } + + // get open offer + OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getOffer().getId()).orElse(null); + if (openOffer == null) throw new RuntimeException("Open offer not found for " + trade.getClass().getSimpleName() + " " + trade.getId()); + Offer offer = openOffer.getOffer(); + + // reset reserve tx state + trade.getSelf().setReserveTxHex(null); + trade.getSelf().setReserveTxHash(null); + trade.getSelf().setReserveTxKey(null); + trade.getSelf().setReserveTxKeyImages(null); + + // recreate reserve tx + log.warn("Maker is recreating reserve tx for tradeId={}", trade.getShortId()); + MoneroTxWallet reserveTx = null; + synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); + + // thaw reserved key images + log.info("Thawing reserve tx key images for tradeId={}", trade.getShortId()); + HavenoUtils.xmrWalletService.thawOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while thawing key images, tradeId=" + trade.getShortId()); + trade.startProtocolTimeout(); + + // collect relevant info + BigInteger makerFee = offer.getMaxMakerFee(); + BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); + BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); + BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, offer.getPenaltyFeePct()); + String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); + XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); + Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); + + // attempt re-creating reserve tx + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, tradeId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); + throw e; + } catch (Exception e) { + log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (reserveTx != null) break; + } + } + } catch (Exception e) { + + // reset state + if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + model.getXmrWalletService().freezeOutputs(offer.getOfferPayload().getReserveTxKeyImages()); + trade.getSelf().setReserveTxKeyImages(null); + throw e; + } + + // reset protocol timeout + trade.startProtocolTimeout(); + + // update state + trade.getSelf().setReserveTxHash(reserveTx.getHash()); + trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); + trade.getSelf().setReserveTxKey(reserveTx.getKey()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + } + + // save process state + processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used? + processModel.getTradeManager().requestPersistence(); + complete(); + } catch (Throwable t) { + trade.setErrorMessage("An error occurred.\n" + + "Error message:\n" + + t.getMessage()); + failed(t); + } + } + + private boolean isTimedOut() { + return !processModel.getTradeManager().hasOpenTrade(trade); + } +} diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java index 9f191131ec..7f3c97df9a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java @@ -131,7 +131,7 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask { takerRequest.getUid(), Version.getP2PMessageVersion(), null, - takerRequest.getCurrentDate(), + trade.getTakeOfferDate().getTime(), // maker's date is used as shared timestamp trade.getMaker().getNodeAddress(), trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 6f10625e35..5ab9a75494 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -113,7 +113,7 @@ public class MaybeSendSignContractRequest extends TradeTask { depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); } catch (Exception e) { log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying @@ -156,6 +156,8 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setDepositTxHex(depositTx.getFullHex()); + trade.getSelf().setDepositTxKey(depositTx.getKey()); trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java index dcaf1a7e76..b994c84800 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositResponse.java @@ -38,13 +38,21 @@ public class ProcessDepositResponse extends TradeTask { try { runInterceptHook(); - // throw if error + // handle error DepositResponse message = (DepositResponse) processModel.getTradeMessage(); if (message.getErrorMessage() != null) { - log.warn("Unregistering trade {} {} because deposit response has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); + log.warn("Deposit response has error message for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); - processModel.getTradeManager().unregisterTrade(trade); - throw new RuntimeException(message.getErrorMessage()); + trade.setInitError(new RuntimeException(message.getErrorMessage())); + complete(); + return; + } + + // publish deposit transaction for redundancy + try { + model.getXmrWalletService().getMonerod().submitTxHex(trade.getSelf().getDepositTxHex()); + } catch (Exception e) { + log.warn("Failed to redundantly publish deposit transaction for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); } // record security deposits diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index 7e0c85af2d..1d3632a74e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -55,7 +55,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // decrypt seller payment account payload if key given if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { - log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key"); + log.info("Decrypting seller payment account payload for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java index 61962ce1d2..a97017d1c2 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -98,6 +98,7 @@ public class ProcessInitTradeRequest extends TradeTask { sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender == trade.getMaker()) { trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); + trade.setTakeOfferDate(request.getCurrentDate()); // check trade price try { @@ -116,6 +117,7 @@ public class ProcessInitTradeRequest extends TradeTask { if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring"); if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); + if (request.getCurrentDate() != trade.getTakeOfferDate().getTime()) throw new RuntimeException("Trade's take offer date does not match request's current date"); } // handle invalid sender @@ -134,6 +136,7 @@ public class ProcessInitTradeRequest extends TradeTask { trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator"); + trade.setTakeOfferDate(request.getCurrentDate()); } // handle invalid trade type diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 5d55bbaea7..adff1a9848 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -41,6 +41,7 @@ import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; +import haveno.core.trade.Trade.State; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.Validator; @@ -51,6 +52,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessPaymentReceivedMessage extends TradeTask { + public ProcessPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -81,11 +83,14 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } + // set state to confirmed payment receipt before processing + trade.advanceState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + // cannot process until wallet sees deposits unlocked if (!trade.isDepositsUnlocked()) { trade.syncAndPollWallet(); if (!trade.isDepositsUnlocked()) { - throw new RuntimeException("Cannot process PaymentReceivedMessage until wallet sees that deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); + throw new RuntimeException("Cannot process PaymentReceivedMessage until the trade wallet sees that the deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } @@ -122,9 +127,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask { complete(); } catch (Throwable t) { - // do not reprocess illegal argument + // handle illegal exception if (HavenoUtils.isIllegal(t)) { - trade.getSeller().setPaymentReceivedMessage(null); // do not reprocess + trade.getSeller().setPaymentReceivedMessage(null); // stops reprocessing trade.requestPersistence(); } @@ -155,8 +160,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // verify and publish payout tx if (!trade.isPayoutPublished()) { try { - boolean isSigned = message.getSignedPayoutTxHex() != null; - if (isSigned) { + if (message.getPayoutTxId() != null && trade.isBuyer()) { + trade.processBuyerPayout(message.getPayoutTxId()); // buyer can validate payout tx by id with main wallet (in case of multisig issues) + } else if (message.getSignedPayoutTxHex() != null) { log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId()); trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true); } else { @@ -177,9 +183,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { else throw e; } } - } else { - log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); - if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true); } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 1f99d64806..7bdd6659ea 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -52,7 +52,7 @@ public class ProcessPaymentSentMessage extends TradeTask { if (!trade.isDepositsConfirmed()) { trade.syncAndPollWallet(); if (!trade.isDepositsConfirmed()) { - throw new RuntimeException("Cannot process PaymentSentMessage until wallet sees that deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); + throw new RuntimeException("Cannot process PaymentSentMessage until the trade wallet sees that the deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index a146fa3419..6e49617724 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -40,48 +40,61 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { // check connection trade.verifyDaemonConnection(); - // handle first time preparation - if (trade.getArbitrator().getPaymentReceivedMessage() == null) { - - // synchronize on lock for wallet operations + // import and export multisig hex if payout already published + if (trade.isPayoutPublished()) { synchronized (trade.getWalletLock()) { - synchronized (HavenoUtils.getWalletFunctionLock()) { - - // import multisig hex unless already signed - if (trade.getPayoutTxHex() == null) { + if (trade.walletExists()) { + synchronized (HavenoUtils.getWalletFunctionLock()) { trade.importMultisigHex(); - } - - // verify, sign, and publish payout tx if given - if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null) { - try { - if (trade.getPayoutTxHex() == null) { - log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId()); - trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true); - } else { - log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId()); - trade.processPayoutTx(trade.getPayoutTxHex(), false, true); - } - } catch (IllegalArgumentException | IllegalStateException e) { - log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); - createUnsignedPayoutTx(); - } catch (Exception e) { - log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e); - throw e; - } - } - - // otherwise create unsigned payout tx - else if (trade.getSelf().getUnsignedPayoutTxHex() == null) { - createUnsignedPayoutTx(); + trade.exportMultisigHex(); } } } - } else if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) { + } else { - // republish payout tx from previous message - log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId()); - trade.processPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true); + // process or create payout tx + if (trade.getPayoutTxHex() == null) { + + // synchronize on lock for wallet operations + synchronized (trade.getWalletLock()) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + + // import multisig hex unless already signed + if (trade.getPayoutTxHex() == null) { + trade.importMultisigHex(); + } + + // verify, sign, and publish payout tx if given + if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null && !trade.getProcessModel().isPaymentSentPayoutTxStale()) { + try { + if (trade.getPayoutTxHex() == null) { + log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId()); + trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true); + } else { + log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId()); + trade.processPayoutTx(trade.getPayoutTxHex(), false, true); + } + } catch (IllegalArgumentException | IllegalStateException e) { + log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e); + createUnsignedPayoutTx(); + } catch (Exception e) { + log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e); + throw e; + } + } + + // otherwise create unsigned payout tx + else if (trade.getSelf().getUnsignedPayoutTxHex() == null) { + createUnsignedPayoutTx(); + } + } + } + } else { + + // republish payout tx from previous message + log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId()); + trade.processPayoutTx(trade.getPayoutTxHex(), false, true); + } } // close open disputes @@ -99,8 +112,14 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { private void createUnsignedPayoutTx() { log.info("Seller creating unsigned payout tx for trade {}", trade.getId()); - MoneroTxWallet payoutTx = trade.createPayoutTx(); - trade.updatePayout(payoutTx); - trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + try { + trade.getProcessModel().setPaymentSentPayoutTxStale(true); + MoneroTxWallet payoutTx = trade.createPayoutTx(); + trade.setPayoutTx(payoutTx); + trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } catch (Exception e) { + if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); + else throw e; + } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index 202d4c8c79..34bb458c1a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -45,6 +45,7 @@ import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; +import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.TradeMailboxMessage; @@ -59,6 +60,8 @@ import static com.google.common.base.Preconditions.checkArgument; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; + @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { @@ -68,6 +71,9 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; + private String unsignedPayoutTxHex = null; + private String signedPayoutTxHex = null; + private String updatedMultisigHex = null; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -90,8 +96,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag try { runInterceptHook(); - // skip if already received - if (isReceived()) { + // skip if stopped + if (stopSending()) { if (!isCompleted()) complete(); return; } @@ -122,20 +128,30 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = HavenoUtils.getDeterministicId(trade, PaymentReceivedMessage.class, getReceiverNodeAddress()); - boolean deferPublishPayout = trade.isPayoutPublished() || trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(); // informs receiver to expect payout so delay processing + boolean deferPublishPayout = getReceiver() == trade.getArbitrator() && (trade.isPayoutPublished() || trade.getOtherPeer(getReceiver()).isPaymentReceivedMessageArrived()); // informs receiver to expect payout so delay processing + unsignedPayoutTxHex = trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null; // signed + signedPayoutTxHex = trade.getPayoutTxHex(); + updatedMultisigHex = trade.getSelf().getUpdatedMultisigHex(); PaymentReceivedMessage message = new PaymentReceivedMessage( tradeId, processModel.getMyNodeAddress(), deterministicId, - trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null, // unsigned // TODO: phase in after next update to clear old style trades - trade.getPayoutTxHex() == null ? null : trade.getPayoutTxHex(), // signed - trade.getSelf().getUpdatedMultisigHex(), + unsignedPayoutTxHex, + signedPayoutTxHex, + updatedMultisigHex, deferPublishPayout, trade.getTradePeer().getAccountAgeWitness(), signedWitness, - getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message + getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null, // buyer already has payment sent message, + trade.getPayoutTxId() ); - checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex"); + + // verify message + if (trade.isPayoutPublished()) { + checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published"); + } else { + checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex"); + } // sign message try { @@ -191,8 +207,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag private void tryToSendAgainLater() { - // skip if already received - if (isReceived()) return; + // skip if stopped + if (stopSending()) return; if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); @@ -226,12 +242,24 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag } private void onMessageStateChange(MessageState newValue) { - if (isReceived()) { + if (isMessageReceived()) { cleanup(); } } - protected boolean isReceived() { + protected boolean isMessageReceived() { return getReceiver().isPaymentReceivedMessageReceived(); } + + protected boolean stopSending() { + if (isMessageReceived()) return true; // stop if message received + if (!trade.isPaymentReceived()) return true; // stop if trade state reset + if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period + + // check if message state is outdated + if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true; + if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true; + if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true; + return false; + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java index 7101c488a5..74153a8dbc 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java @@ -82,8 +82,8 @@ public class SendDepositRequest extends TradeTask { Version.getP2PMessageVersion(), new Date().getTime(), trade.getSelf().getContractSignature(), - trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(), - trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(), + trade.getSelf().getDepositTxHex(), + trade.getSelf().getDepositTxKey(), trade.getSelf().getPaymentAccountKey()); // update trade state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 9a461b2d83..976e7a4fdc 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -57,10 +57,10 @@ public class TakerReserveTradeFunds extends TradeTask { trade.startProtocolTimeout(); // collect relevant info - BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); BigInteger takerFee = trade.getTakerFee(); BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee(); + BigInteger penaltyFee = HavenoUtils.multiply(securityDeposit, trade.getOffer().getPenaltyFeePct()); String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // attempt creating reserve tx @@ -75,7 +75,7 @@ public class TakerReserveTradeFunds extends TradeTask { throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying @@ -88,7 +88,7 @@ public class TakerReserveTradeFunds extends TradeTask { } } catch (Exception e) { - // reset state with wallet lock + // reset state model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); @@ -101,11 +101,12 @@ public class TakerReserveTradeFunds extends TradeTask { // reset protocol timeout trade.startProtocolTimeout(); - // update trade state - trade.getTaker().setReserveTxHash(reserveTx.getHash()); - trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); - trade.getTaker().setReserveTxKey(reserveTx.getKey()); - trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + // update state + trade.getSelf().setReserveTxHash(reserveTx.getHash()); + trade.getSelf().setReserveTxHex(reserveTx.getFullHex()); + trade.getSelf().setReserveTxKey(reserveTx.getKey()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx)); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java b/core/src/main/java/haveno/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java index b7d07190a8..a0efc3ceaa 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java @@ -63,7 +63,7 @@ public class VerifyPeersAccountAgeWitness extends TradeTask { // only verify traditional offer Offer offer = checkNotNull(trade.getOffer()); - if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) { + if (CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode())) { complete(); return; } diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 209fad38cf..e4d379f70d 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -28,6 +28,7 @@ import haveno.common.util.CollectionUtils; import haveno.common.util.ExtraDataMapValidator; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; +import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; @@ -35,6 +36,7 @@ import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; +import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.storage.payload.CapabilityRequiringPayload; @@ -67,20 +69,42 @@ import static com.google.common.base.Preconditions.checkNotNull; public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayload, PersistableNetworkPayload, CapabilityRequiringPayload, DateSortedTruncatablePayload { + private static final String VERSION_KEY = "v"; // single character key for versioning + @JsonExclude private transient static final ZoneId ZONE_ID = ZoneId.systemDefault(); - private static final double FUZZ_AMOUNT_PCT = 0.05; - private static final int FUZZ_DATE_HOURS = 24; - public static TradeStatistics3 from(Trade trade, - @Nullable String referralId, - boolean isTorNetworkNode, - boolean isFuzzed) { + public static TradeStatistics3 fromV0(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.0, 0, 0); + } + + public static TradeStatistics3 fromV1(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.05, 24, 0); + } + + public static TradeStatistics3 fromV2(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + return from(trade, referralId, isTorNetworkNode, 0.10, 48, .01); + } + + // randomize completed trade info #1099 + private static TradeStatistics3 from(Trade trade, + @Nullable String referralId, + boolean isTorNetworkNode, + double fuzzAmountPct, + int fuzzDateHours, + double fuzzPricePct) { Map extraDataMap = new HashMap<>(); if (referralId != null) { extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); } + // Store the trade protocol version to denote that the crypto price is not inverted starting with v3. + // This can be removed in the future after all stats are expected to not be inverted, + // then only stats which are missing this field prior to then need to be uninverted. + if (!trade.getOffer().isInverted() && CurrencyUtil.isCryptoCurrency(trade.getOffer().getCounterCurrencyCode())) { + extraDataMap.put(VERSION_KEY, trade.getOffer().getOfferPayload().getProtocolVersion() + ""); + } + NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress(), "Arbitrator address is null", trade.getClass().getSimpleName(), trade.getId()); // The first 4 chars are sufficient to identify an arbitrator. @@ -91,28 +115,40 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl arbitratorNodeAddress.getFullAddress(); Offer offer = checkNotNull(trade.getOffer()); - return new TradeStatistics3(offer.getCurrencyCode(), - trade.getPrice().getValue(), - isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(), + return new TradeStatistics3(offer.getCounterCurrencyCode(), + fuzzTradePriceReproducibly(trade, fuzzPricePct), + fuzzTradeAmountReproducibly(trade, fuzzAmountPct), offer.getPaymentMethod().getId(), - isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(), + fuzzTradeDateReproducibly(trade, fuzzDateHours), truncatedArbitratorNodeAddress, extraDataMap); } - private static long fuzzTradeAmountReproducibly(Trade trade) { // randomize completed trade info #1099 + private static long fuzzTradePriceReproducibly(Trade trade, double fuzzPricePct) { + if (fuzzPricePct == 0.0) return trade.getRawPrice().getValue(); long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long exactPrice = trade.getRawPrice().getValue(); + long adjustedPrice = (long) random.nextDouble(exactPrice * (1.0 - fuzzPricePct), exactPrice * (1.0 + fuzzPricePct)); + log.debug("trade {} fuzzed trade price for tradeStatistics is {}", trade.getShortId(), adjustedPrice); + return adjustedPrice; + } + + private static long fuzzTradeAmountReproducibly(Trade trade, double fuzzAmountPct) { + if (fuzzAmountPct == 0.0) return trade.getAmount().longValueExact(); + long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp long exactAmount = trade.getAmount().longValueExact(); - Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp - long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT)); + long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - fuzzAmountPct), exactAmount * (1.0 + fuzzAmountPct)); log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); return adjustedAmount; } - private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 + private static long fuzzTradeDateReproducibly(Trade trade, int fuzzDateHours) { + if (fuzzDateHours == 0) return trade.getTakeOfferDate().getTime(); long originalTimestamp = trade.getTakeOfferDate().getTime(); - Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp - long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS), originalTimestamp); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(fuzzDateHours), originalTimestamp); log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); return adjustedTimestamp; } @@ -121,6 +157,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not // recognized. + // NOTE: Only ***UNUSED*** payment methods can be added here, otherwise historical stats will have a different hash than new stats. private enum PaymentMethodMapper { OK_PAY, CASH_APP, @@ -183,12 +220,10 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl @Getter private final String currency; - @Getter private final long price; @Getter - private final long amount; // BTC amount + private final long amount; // XMR amount private final String paymentMethod; - // As only seller is publishing it is the sellers trade date private final long date; // Old converted trade stat objects might not have it set @@ -202,7 +237,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl // Hash get set in constructor from json of all the other data fields (with hash = null). @JsonExclude private final byte[] hash; - // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // Should be only used in exceptional case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable @@ -288,8 +323,6 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl public byte[] createHash() { // We create hash from all fields excluding hash itself. We use json as simple data serialisation. - // TradeDate is different for both peers so we ignore it for hash. ExtraDataMap is ignored as well as at - // software updates we might have different entries which would cause a different hash. return Hash.getSha256Ripemd160hash(JsonUtil.objectToJson(this).getBytes(Charsets.UTF_8)); } @@ -385,11 +418,26 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl public Price getTradePrice() { if (priceObj == null) { - priceObj = Price.valueOf(currency, price); + priceObj = Price.valueOf(currency, getNormalizedPrice()); } return priceObj; } + /** + * Returns the price as XMR/QUOTE. + * + * Note: Cannot override getPrice() because it's used for gson serialization, nor do we want expose it publicly. + */ + public long getNormalizedPrice() { + return isInverted() ? PriceUtil.invertLongPrice(price, currency) : price; + } + + private boolean isInverted() { + return CurrencyUtil.isCryptoCurrency(currency) && + (extraDataMap == null || + !extraDataMap.containsKey(VERSION_KEY)); // crypto price is inverted if missing key + } + public BigInteger getTradeAmount() { return BigInteger.valueOf(amount); } @@ -446,7 +494,8 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl public String toString() { return "TradeStatistics3{" + "\n currency='" + currency + '\'' + - ",\n price=" + price + + ",\n rawPrice=" + price + + ",\n normalizedPrice=" + getNormalizedPrice() + ",\n amount=" + amount + ",\n paymentMethod='" + paymentMethod + '\'' + ",\n date=" + date + diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsForJson.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsForJson.java index 774726d259..c2d1294d0b 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsForJson.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsForJson.java @@ -17,7 +17,6 @@ package haveno.core.trade.statistics; -import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; @@ -39,7 +38,7 @@ public final class TradeStatisticsForJson { public final long tradeDate; public final String paymentMethod; - // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) + // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app XMR is always in the focus) public String currencyPair; public long primaryMarketTradePrice; @@ -49,27 +48,18 @@ public final class TradeStatisticsForJson { public TradeStatisticsForJson(TradeStatistics3 tradeStatistics) { this.currency = tradeStatistics.getCurrency(); this.paymentMethod = tradeStatistics.getPaymentMethodId(); - this.tradePrice = tradeStatistics.getPrice(); + this.tradePrice = tradeStatistics.getNormalizedPrice(); this.tradeAmount = tradeStatistics.getAmount(); this.tradeDate = tradeStatistics.getDateAsLong(); try { Price tradePrice = getPrice(); - if (CurrencyUtil.isCryptoCurrency(currency)) { - currencyPair = currency + "/" + Res.getBaseCurrencyCode(); - primaryMarketTradePrice = tradePrice.getValue(); - primaryMarketTradeAmount = getTradeVolume() != null ? - getTradeVolume().getValue() : - 0; - primaryMarketTradeVolume = getTradeAmount().longValueExact(); - } else { - currencyPair = Res.getBaseCurrencyCode() + "/" + currency; - primaryMarketTradePrice = tradePrice.getValue(); - primaryMarketTradeAmount = getTradeAmount().longValueExact(); - primaryMarketTradeVolume = getTradeVolume() != null ? - getTradeVolume().getValue() : - 0; - } + currencyPair = Res.getBaseCurrencyCode() + "/" + currency; + primaryMarketTradePrice = tradePrice.getValue(); + primaryMarketTradeAmount = getTradeAmount().longValueExact(); + primaryMarketTradeVolume = getTradeVolume() != null ? + getTradeVolume().getValue() : + 0; } catch (Throwable t) { log.error(t.getMessage()); t.printStackTrace(); diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java index b85e6932e6..c6ff8e71ee 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java @@ -20,12 +20,15 @@ package haveno.core.trade.statistics; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; import haveno.core.locale.CurrencyTuple; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.util.JsonUtil; import haveno.network.p2p.P2PService; @@ -54,6 +57,7 @@ public class TradeStatisticsManager { private final boolean dumpStatistics; private final ObservableSet observableTradeStatisticsSet = FXCollections.observableSet(); private JsonFileManager jsonFileManager; + public static final int PUBLISH_STATS_RANDOM_DELAY_HOURS = 24; @Inject public TradeStatisticsManager(P2PService p2PService, @@ -68,8 +72,8 @@ public class TradeStatisticsManager { this.storageDir = storageDir; this.dumpStatistics = dumpStatistics; - appendOnlyDataStoreService.addService(tradeStatistics3StorageService); + HavenoUtils.tradeStatisticsManager = this; } public void shutDown() { @@ -100,8 +104,8 @@ public class TradeStatisticsManager { .collect(Collectors.toSet()); - // remove duplicates in early trades due to bug - deduplicateEarlyTradeStatistics(set); + // remove duplicates in early trade stats due to bugs + removeDuplicateStats(set); synchronized (observableTradeStatisticsSet) { observableTradeStatisticsSet.addAll(set); @@ -110,7 +114,42 @@ public class TradeStatisticsManager { maybeDumpStatistics(); } - private void deduplicateEarlyTradeStatistics(Set tradeStats) { + private void removeDuplicateStats(Set tradeStats) { + removeEarlyDuplicateStats(tradeStats); + removeEarlyDuplicateStatsFuzzy(tradeStats); + } + + private void removeEarlyDuplicateStats(Set tradeStats) { + + // collect trades before September 30, 2024 + Set earlyTrades = tradeStats.stream() + .filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-09-30T00:00:00Z"))) + .collect(Collectors.toSet()); + + // collect stats with duplicated timestamp, currency, and payment method + Set duplicates = new HashSet<>(); + Set deduplicates = new HashSet<>(); + for (TradeStatistics3 tradeStatistic : earlyTrades) { + TradeStatistics3 duplicate = findDuplicate(tradeStatistic, deduplicates); + if (duplicate == null) deduplicates.add(tradeStatistic); + else duplicates.add(tradeStatistic); + } + + // remove duplicated stats + tradeStats.removeAll(duplicates); + } + + private TradeStatistics3 findDuplicate(TradeStatistics3 tradeStatistics, Set set) { + return set.stream().filter(e -> isDuplicate(tradeStatistics, e)).findFirst().orElse(null); + } + + private boolean isDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { + if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false; + if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false; + return tradeStatistics1.getDateAsLong() == tradeStatistics2.getDateAsLong(); + } + + private void removeEarlyDuplicateStatsFuzzy(Set tradeStats) { // collect trades before August 7, 2024 Set earlyTrades = tradeStats.stream() @@ -126,7 +165,7 @@ public class TradeStatisticsManager { else duplicates.add(tradeStatistic); } - // remove duplicated trades + // remove duplicated stats tradeStats.removeAll(duplicates); } @@ -137,7 +176,7 @@ public class TradeStatisticsManager { private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) { if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false; if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false; - if (tradeStatistics1.getPrice() != tradeStatistics2.getPrice()) return false; + if (tradeStatistics1.getNormalizedPrice() != tradeStatistics2.getNormalizedPrice()) return false; return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2); } @@ -208,7 +247,13 @@ public class TradeStatisticsManager { jsonFileManager.writeToDiscThreaded(JsonUtil.objectToJson(array), "trade_statistics"); } - public void maybeRepublishTradeStatistics(Set trades, + public void maybePublishTradeStatistics(Trade trade, @Nullable String referralId, boolean isTorNetworkNode) { + Set trades = new HashSet<>(); + trades.add(trade); + maybePublishTradeStatistics(trades, referralId, isTorNetworkNode); + } + + public void maybePublishTradeStatistics(Set trades, @Nullable String referralId, boolean isTorNetworkNode) { long ts = System.currentTimeMillis(); @@ -219,38 +264,50 @@ public class TradeStatisticsManager { return; } - TradeStatistics3 tradeStatistics3 = null; + TradeStatistics3 tradeStatistics3V0 = null; try { - tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false); + tradeStatistics3V0 = TradeStatistics3.fromV0(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } - TradeStatistics3 tradeStatistics3Fuzzed = null; + TradeStatistics3 tradeStatistics3V1 = null; try { - tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true); + tradeStatistics3V1 = TradeStatistics3.fromV1(trade, referralId, isTorNetworkNode); } catch (Exception e) { log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); return; } - boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); - boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash())); - if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) { + TradeStatistics3 tradeStatistics3V2 = null; + try { + tradeStatistics3V2 = TradeStatistics3.fromV2(trade, referralId, isTorNetworkNode); + } catch (Exception e) { + log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); + return; + } + + boolean hasTradeStatistics3V0 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V0.getHash())); + boolean hasTradeStatistics3V1 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V1.getHash())); + boolean hasTradeStatistics3V2 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3V2.getHash())); + if (hasTradeStatistics3V0 || hasTradeStatistics3V1 || hasTradeStatistics3V2) { log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", trade.getShortId()); return; } - if (!tradeStatistics3.isValid()) { - log.warn("Trade: {}. Trade statistics is invalid. We do not publish it.", tradeStatistics3); + if (!tradeStatistics3V2.isValid()) { + log.warn("Trade statistics are invalid for {} {}. We do not publish: {}", trade.getClass().getSimpleName(), trade.getShortId(), tradeStatistics3V1); return; } - log.info("Trade: {}. We republish tradeStatistics3 as we did not find it in the existing trade statistics. ", - trade.getShortId()); - p2PService.addPersistableNetworkPayload(tradeStatistics3, true); + // publish after random delay within 12 hours + log.info("Scheduling to publish trade statistics at random time for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); + TradeStatistics3 tradeStatistics3V2Final = tradeStatistics3V2; + UserThread.runAfterRandomDelay(() -> { + p2PService.addPersistableNetworkPayload(tradeStatistics3V2Final, true); + }, 0, PUBLISH_STATS_RANDOM_DELAY_HOURS / 2 * 60 * 60 * 1000, TimeUnit.MILLISECONDS); }); log.info("maybeRepublishTradeStatistics took {} ms. Number of tradeStatistics: {}. Number of own trades: {}", System.currentTimeMillis() - ts, hashes.size(), trades.size()); diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 02b1d82aaf..cf074d395b 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -109,8 +109,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; - public static final int CLEAR_DATA_AFTER_DAYS_INITIAL = 99999; // feature effectively disabled until user agrees to settings notification - public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used when user has agreed to settings notification + public static final int CLEAR_DATA_AFTER_DAYS_DEFAULT = 60; // used with new instance or when existing user has agreed to settings notification + public static final int CLEAR_DATA_AFTER_DAYS_DISABLED = 99999; // feature effectively disabled until existing user agrees to settings notification // payload is initialized so the default values are available for Property initialization. @@ -309,6 +309,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid setIgnoreDustThreshold(600); } + if (prefPayload.getClearDataAfterDays() < 1) { + setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED); + } + // For users from old versions the 4 flags a false but we want to have it true by default // PhoneKeyAndToken is also null so we can use that to enable the flags if (prefPayload.getPhoneKeyAndToken() == null) { @@ -619,8 +623,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid } public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) { - double max = Restrictions.getMaxSecurityDepositAsPercent(); - double min = Restrictions.getMinSecurityDepositAsPercent(); + double max = Restrictions.getMaxSecurityDepositPct(); + double min = Restrictions.getMinSecurityDepositPct(); if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent))); @@ -849,12 +853,12 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent(); - if (value < Restrictions.getMinSecurityDepositAsPercent()) { - value = Restrictions.getMinSecurityDepositAsPercent(); + if (value < Restrictions.getMinSecurityDepositPct()) { + value = Restrictions.getMinSecurityDepositPct(); setSecurityDepositAsPercent(value, paymentAccount); } - return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : value; + return value == 0 ? Restrictions.getDefaultSecurityDepositPct() : value; } @Override diff --git a/core/src/main/java/haveno/core/user/PreferencesPayload.java b/core/src/main/java/haveno/core/user/PreferencesPayload.java index 44e6aef509..5bcc478a39 100644 --- a/core/src/main/java/haveno/core/user/PreferencesPayload.java +++ b/core/src/main/java/haveno/core/user/PreferencesPayload.java @@ -41,7 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositPct; @Slf4j @Data @@ -58,7 +58,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private boolean autoSelectArbitrators = true; private Map dontShowAgainMap = new HashMap<>(); private boolean tacAccepted; - private boolean splitOfferOutput = false; + private boolean splitOfferOutput = true; private boolean showOwnOffersInOfferBook = true; @Nullable private TradeCurrency preferredTradeCurrency; @@ -120,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String rpcPw; @Nullable private String takeOfferSelectedPaymentAccountId; - private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent(); + private double securityDepositAsPercent = getDefaultSecurityDepositPct(); private int ignoreDustThreshold = 600; - private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; - private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent(); + private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT; + private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositPct(); private int blockNotifyPort; private boolean tacAcceptedV120; private double bsqAverageTrimThreshold = 0.05; diff --git a/core/src/main/java/haveno/core/util/AveragePriceUtil.java b/core/src/main/java/haveno/core/util/AveragePriceUtil.java index b548c0120f..3e30327b85 100644 --- a/core/src/main/java/haveno/core/util/AveragePriceUtil.java +++ b/core/src/main/java/haveno/core/util/AveragePriceUtil.java @@ -66,15 +66,15 @@ public class AveragePriceUtil { private static List removeOutliers(List list, double percentToTrim) { List yValues = list.stream() .filter(TradeStatistics3::isValid) - .map(e -> (double) e.getPrice()) + .map(e -> (double) e.getNormalizedPrice()) .collect(Collectors.toList()); Tuple2 tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER); double lowerBound = tuple.first; double upperBound = tuple.second; return list.stream() - .filter(e -> e.getPrice() > lowerBound) - .filter(e -> e.getPrice() < upperBound) + .filter(e -> e.getNormalizedPrice() > lowerBound) + .filter(e -> e.getNormalizedPrice() < upperBound) .collect(Collectors.toList()); } diff --git a/core/src/main/java/haveno/core/util/PriceUtil.java b/core/src/main/java/haveno/core/util/PriceUtil.java index a91b13c85c..14065afe18 100644 --- a/core/src/main/java/haveno/core/util/PriceUtil.java +++ b/core/src/main/java/haveno/core/util/PriceUtil.java @@ -35,6 +35,9 @@ import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; import haveno.core.util.validation.MonetaryValidator; + +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -84,7 +87,7 @@ public class PriceUtil { } public boolean hasMarketPrice(Offer offer) { - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); checkNotNull(priceFeedService, "priceFeed must not be null"); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); Price price = offer.getPrice(); @@ -103,7 +106,7 @@ public class PriceUtil { return Optional.empty(); } - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); checkNotNull(priceFeedService, "priceFeed must not be null"); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); double marketPriceAsDouble = checkNotNull(marketPrice).getPrice(); @@ -114,7 +117,7 @@ public class PriceUtil { double marketPrice, OfferDirection direction) { // If the offer did not use % price we calculate % from current market price - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); Price price = offer.getPrice(); int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : @@ -123,29 +126,15 @@ public class PriceUtil { double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); double value; if (direction == OfferDirection.SELL) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - if (marketPrice == 0) { - return Optional.empty(); - } - value = 1 - scaled / marketPrice; - } else { - if (marketPrice == 1) { - return Optional.empty(); - } - value = scaled / marketPrice - 1; + if (marketPrice == 0) { + return Optional.empty(); } + value = 1 - scaled / marketPrice; } else { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - if (marketPrice == 1) { - return Optional.empty(); - } - value = scaled / marketPrice - 1; - } else { - if (marketPrice == 0) { - return Optional.empty(); - } - value = 1 - scaled / marketPrice; + if (marketPrice == 1) { + return Optional.empty(); } + value = scaled / marketPrice - 1; } return Optional.of(value); } @@ -183,4 +172,13 @@ public class PriceUtil { return CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; } + + public static long invertLongPrice(long price, String currencyCode) { + if (price == 0) return 0; + int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; + double priceDouble = MathUtils.scaleDownByPowerOf10(price, precision); + double priceDoubleInverted = BigDecimal.ONE.divide(BigDecimal.valueOf(priceDouble), precision, RoundingMode.HALF_UP).doubleValue(); + double scaled = MathUtils.scaleUpByPowerOf10(priceDoubleInverted, precision); + return MathUtils.roundDoubleToLong(scaled); + } } diff --git a/core/src/main/java/haveno/core/util/VolumeUtil.java b/core/src/main/java/haveno/core/util/VolumeUtil.java index b74a70f226..050b7b1a4d 100644 --- a/core/src/main/java/haveno/core/util/VolumeUtil.java +++ b/core/src/main/java/haveno/core/util/VolumeUtil.java @@ -51,6 +51,7 @@ import org.bitcoinj.utils.MonetaryFormat; import java.math.BigInteger; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.Collection; import java.util.Locale; public class VolumeUtil { @@ -187,4 +188,35 @@ public class VolumeUtil { private static MonetaryFormat getMonetaryFormat(String currencyCode) { return CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode) ? VOLUME_FORMAT_UNIT : VOLUME_FORMAT_PRECISE; } + + public static Volume sum(Collection volumes) { + if (volumes == null || volumes.isEmpty()) { + return null; + } + Volume sum = null; + for (Volume volume : volumes) { + if (sum == null) { + sum = volume; + } else { + if (!sum.getCurrencyCode().equals(volume.getCurrencyCode())) { + throw new IllegalArgumentException("Cannot sum volumes with different currencies"); + } + sum = add(sum, volume); + } + } + return sum; + } + + public static Volume add(Volume volume1, Volume volume2) { + if (volume1 == null) return volume2; + if (volume2 == null) return volume1; + if (!volume1.getCurrencyCode().equals(volume2.getCurrencyCode())) { + throw new IllegalArgumentException("Cannot add volumes with different currencies"); + } + if (volume1.getMonetary() instanceof CryptoMoney) { + return new Volume(((CryptoMoney) volume1.getMonetary()).add((CryptoMoney) volume2.getMonetary())); + } else { + return new Volume(((TraditionalMoney) volume1.getMonetary()).add((TraditionalMoney) volume2.getMonetary())); + } + } } diff --git a/core/src/main/java/haveno/core/util/coin/CoinUtil.java b/core/src/main/java/haveno/core/util/coin/CoinUtil.java index 6c163ede6a..bc079c9b5e 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -75,19 +75,19 @@ public class CoinUtil { return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger(); } - public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { + public static BigInteger getRoundedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, String currencyCode, String paymentMethodId) { if (price != null) { if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { - return getRoundedAtmCashAmount(amount, price, maxTradeLimit); + return getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { - return getRoundedAmountUnit(amount, price, maxTradeLimit); + return getRoundedAmountUnit(amount, price, minAmount, maxAmount); } } - return getRoundedAmount4Decimals(amount, maxTradeLimit); + return getRoundedAmount4Decimals(amount); } - public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 10); + public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { + return getAdjustedAmount(amount, price, minAmount, maxAmount, 10); } /** @@ -96,14 +96,15 @@ public class CoinUtil { * * @param amount Monero amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in atomic units. + * @param minAmount The minimum amount. + * @param maxAmount The maximum amount. * @return The adjusted amount */ - public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 1); + public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount) { + return getAdjustedAmount(amount, price, minAmount, maxAmount, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) { + public static BigInteger getRoundedAmount4Decimals(BigInteger amount) { DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); @@ -115,44 +116,51 @@ public class CoinUtil { * * @param amount amount which is a candidate for getting rounded. * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @param minAmount The minimum amount. + * @param maxAmount The maximum amount. * @param factor The factor used for rounding. E.g. 1 means rounded to units of * 1 EUR, 10 means rounded to 10 EUR, etc. * @return The adjusted amount */ @VisibleForTesting - static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) { + static BigInteger getAdjustedAmount(BigInteger amount, Price price, BigInteger minAmount, BigInteger maxAmount, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), - "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" + "amount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" ); + if (minAmount == null) minAmount = Restrictions.getMinTradeAmount(); + checkArgument( + minAmount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), + "minAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(minAmount) + " xmr" + ); + if (maxAmount != null) { + checkArgument( + amount.longValueExact() <= maxAmount.longValueExact(), + "amount must be below maximum of " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(amount) + " xmr" + ); + checkArgument( + maxAmount.longValueExact() >= minAmount.longValueExact(), + "maxAmount must be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr but was " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " xmr" + ); + } + checkArgument( factor > 0, - "factor needs to be positive" + "factor must be positive" ); - // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or - // 10 EUR in case of HalCash. + + // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or 10 EUR in case of HalCash. Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); - if (smallestUnitForVolume.getValue() <= 0) - return BigInteger.ZERO; - + if (smallestUnitForVolume.getValue() <= 0) return BigInteger.ZERO; BigInteger smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); - long minTradeAmount = Restrictions.getMinTradeAmount().longValueExact(); - - checkArgument( - minTradeAmount >= Restrictions.getMinTradeAmount().longValueExact(), - "MinTradeAmount must be at least " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" - ); - smallestUnitForAmount = BigInteger.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.longValueExact())); - // We don't allow smaller amount values than smallestUnitForAmount - boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; + smallestUnitForAmount = BigInteger.valueOf(Math.max(minAmount.longValueExact(), smallestUnitForAmount.longValueExact())); // We get the adjusted volume from our amount + boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; Volume volume = useSmallestUnitForAmount ? getAdjustedVolumeUnit(price.getVolumeByAmount(smallestUnitForAmount), factor) : getAdjustedVolumeUnit(price.getVolumeByAmount(amount), factor); - if (volume.getValue() <= 0) - return BigInteger.ZERO; + if (volume.getValue() <= 0) return BigInteger.ZERO; // From that adjusted volume we calculate back the amount. It might be a bit different as // the amount used as input before due rounding. @@ -161,15 +169,23 @@ public class CoinUtil { // For the amount we allow only 4 decimal places long adjustedAmount = HavenoUtils.centinerosToAtomicUnits(Math.round(HavenoUtils.atomicUnitsToCentineros(amountByVolume) / 10000d) * 10000).longValueExact(); - // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + // If we are below the minAmount we increase the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); - if (maxTradeLimit != null) { - while (adjustedAmount > maxTradeLimit) { + if (minAmount != null) { + while (adjustedAmount < minAmount.longValueExact()) { + adjustedAmount += smallestUnitForAmountUnadjusted.longValueExact(); + } + } + + // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + if (maxAmount != null) { + while (adjustedAmount > maxAmount.longValueExact()) { adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); } } - adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + + adjustedAmount = Math.max(minAmount.longValueExact(), adjustedAmount); + if (maxAmount != null) adjustedAmount = Math.min(maxAmount.longValueExact(), adjustedAmount); return BigInteger.valueOf(adjustedAmount); } } diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index fe49b941fd..2cd664eae9 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -37,7 +37,6 @@ package haveno.core.xmr; import com.google.inject.Inject; import haveno.common.ThreadUtils; -import haveno.common.UserThread; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; @@ -163,19 +162,13 @@ public class Balances { // calculate reserved balance reservedBalance = reservedOfferBalance.add(reservedTradeBalance); - // notify balance update - UserThread.execute(() -> { - - // check if funds received - boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; - if (fundsReceived) { - HavenoUtils.playCashRegisterSound(); - } - - // increase counter to notify listeners - updateCounter.set(updateCounter.get() + 1); - }); + // play sound if funds received + boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; + if (fundsReceived) HavenoUtils.playCashRegisterSound(); } + + // notify balance update + updateCounter.set(updateCounter.get() + 1); } } diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index a8fa1ade26..d932022f9e 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -83,12 +83,21 @@ public class XmrNodes { return Arrays.asList( new XmrNode(MoneroNodesOption.PUBLIC, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "p2pmd.xmrvsbeast.com", 18081, 2, "@xmrvsbeast"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.monerodevs.org", 18089, 2, "@monerodevs.org"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "rucknium.me", 18081, 2, "@Rucknium"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy") + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "selsta1.featherwallet.net", 18081, 2, "@selsta"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "selsta2.featherwallet.net", 18081, 2, "@selsta"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.xmr.ru", 18081, 2, "@xmr.ru"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr.stormycloud.org", 18089, 2, "@stormycloud"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "ravfx.its-a-node.org", 18081, 2, "@ravfx"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "ravfx2.its-a-node.org", 18089, 2, "@ravfx") + // new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 18089, 3, "@plowsof"), // onions tend to have poorer performance + // new XmrNode(MoneroNodesOption.PUBLIC, null, "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion", null, 18081, 3, "@cakewallet") ); default: throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork()); @@ -184,10 +193,6 @@ public class XmrNodes { this.operator = operator; } - public boolean hasOnionAddress() { - return onionAddress != null; - } - public String getHostNameOrAddress() { if (hostName != null) return hostName; @@ -195,10 +200,19 @@ public class XmrNodes { return address; } + public boolean hasOnionAddress() { + return onionAddress != null; + } + public boolean hasClearNetAddress() { return hostName != null || address != null; } + public String getClearNetUri() { + if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address"); + return "http://" + getHostNameOrAddress() + ":" + port; + } + @Override public String toString() { return "onionAddress='" + onionAddress + '\'' + diff --git a/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java b/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java index 2b88b30cd7..8e70245ca7 100644 --- a/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java +++ b/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java @@ -11,7 +11,9 @@ public class DownloadListener { private final DoubleProperty percentage = new SimpleDoubleProperty(-1); public void progress(double percentage, long blocksLeft, Date date) { - UserThread.await(() -> this.percentage.set(percentage)); + UserThread.execute(() -> { + UserThread.await(() -> this.percentage.set(percentage)); // TODO: these awaits are jenky + }); } public void doneDownload() { diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index 5cd181a4aa..3fa7410617 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -26,12 +26,12 @@ import java.math.BigInteger; public class Restrictions { // configure restrictions - public static final double MIN_SECURITY_DEPOSIT_PCT = 0.15; - public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; - public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); - public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); - public static int MAX_EXTRA_INFO_LENGTH = 1500; - public static int MAX_OFFERS_WITH_SHARED_FUNDS = 10; + private static final double MIN_SECURITY_DEPOSIT_PCT = 0.15; + private static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; + private static final BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.05); + private static final BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); + private static final int MAX_EXTRA_INFO_LENGTH = 1500; + private static final int MAX_OFFERS_WITH_SHARED_FUNDS = 10; // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. @@ -57,15 +57,15 @@ public class Restrictions { return MIN_TRADE_AMOUNT; } - public static double getDefaultSecurityDepositAsPercent() { + public static double getDefaultSecurityDepositPct() { return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMinSecurityDepositAsPercent() { + public static double getMinSecurityDepositPct() { return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMaxSecurityDepositAsPercent() { + public static double getMaxSecurityDepositPct() { return MAX_SECURITY_DEPOSIT_PCT; } @@ -73,6 +73,14 @@ public class Restrictions { return MIN_SECURITY_DEPOSIT; } + public static int getMaxExtraInfoLength() { + return MAX_EXTRA_INFO_LENGTH; + } + + public static int getMaxOffersWithSharedFunds() { + return MAX_OFFERS_WITH_SHARED_FUNDS; + } + // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT public static BigInteger getMinRefundAtMediatedDispute() { if (MIN_REFUND_AT_MEDIATED_DISPUTE == null) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 1cde84152c..a56ef54ddb 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -40,14 +41,17 @@ import haveno.core.trade.HavenoUtils; @Slf4j public class XmrKeyImagePoller { - private MoneroDaemon daemon; + private MoneroDaemon monerod; private long refreshPeriodMs; + private Object lock = new Object(); private Map> keyImageGroups = new HashMap>(); + private LinkedHashSet keyImagePollQueue = new LinkedHashSet<>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; private Long lastLogPollErrorTimestamp; + private static final int MAX_POLL_SIZE = 200; /** * Construct the listener. @@ -59,12 +63,12 @@ public class XmrKeyImagePoller { /** * Construct the listener. * - * @param daemon - the Monero daemon to poll + * @param monerod - the Monero daemon to poll * @param refreshPeriodMs - refresh period in milliseconds */ - public XmrKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs) { + public XmrKeyImagePoller(MoneroDaemon monerod, long refreshPeriodMs) { looper = new TaskLooper(() -> poll()); - setDaemon(daemon); + setMonerod(monerod); setRefreshPeriodMs(refreshPeriodMs); } @@ -74,8 +78,10 @@ public class XmrKeyImagePoller { * @param listener - the listener to add */ public void addListener(XmrKeyImageListener listener) { - listeners.add(listener); - refreshPolling(); + synchronized (lock) { + listeners.add(listener); + refreshPolling(); + } } /** @@ -84,18 +90,20 @@ public class XmrKeyImagePoller { * @param listener - the listener to remove */ public void removeListener(XmrKeyImageListener listener) { - if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); - listeners.remove(listener); - refreshPolling(); + synchronized (lock) { + if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); + listeners.remove(listener); + refreshPolling(); + } } /** * Set the Monero daemon to fetch key images from. * - * @param daemon - the daemon to fetch key images from + * @param monerod - the daemon to fetch key images from */ - public void setDaemon(MoneroDaemon daemon) { - this.daemon = daemon; + public void setMonerod(MoneroDaemon monerod) { + this.monerod = monerod; } /** @@ -103,8 +111,8 @@ public class XmrKeyImagePoller { * * @return the daemon to fetch key images from */ - public MoneroDaemon getDaemon() { - return daemon; + public MoneroDaemon getMonerod() { + return monerod; } /** @@ -140,10 +148,11 @@ public class XmrKeyImagePoller { * @param keyImages - key images to listen to */ public void addKeyImages(Collection keyImages, String groupId) { - synchronized (this.keyImageGroups) { + synchronized (lock) { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); + keyImagePollQueue.addAll(keyImages); refreshPolling(); } } @@ -154,29 +163,32 @@ public class XmrKeyImagePoller { * @param keyImages - key images to unlisten to */ public void removeKeyImages(Collection keyImages, String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImagesGroup.removeAll(keyImages); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); - synchronized (lastStatuses) { - for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); + Set allKeyImages = getKeyImages(); + for (String keyImage : keyImages) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); + } } refreshPolling(); } } public void removeKeyImages(String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImageGroups.remove(groupId); - Set keyImages = getKeyImages(); - synchronized (lastStatuses) { - for (String keyImage : keyImagesGroup) { - if (lastStatuses.containsKey(keyImage) && !keyImages.contains(keyImage)) { - lastStatuses.remove(keyImage); - } + Set allKeyImages = getKeyImages(); + for (String keyImage : keyImagesGroup) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); } } refreshPolling(); @@ -187,11 +199,10 @@ public class XmrKeyImagePoller { * Clear the key images which stops polling. */ public void clearKeyImages() { - synchronized (keyImageGroups) { + synchronized (lock) { keyImageGroups.clear(); - synchronized (lastStatuses) { - lastStatuses.clear(); - } + keyImagePollQueue.clear(); + lastStatuses.clear(); refreshPolling(); } } @@ -203,7 +214,7 @@ public class XmrKeyImagePoller { * @return true if the key is spent, false if unspent, null if unknown */ public Boolean isSpent(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { if (!lastStatuses.containsKey(keyImage)) return null; return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } @@ -226,22 +237,22 @@ public class XmrKeyImagePoller { * @return the last known spent status of the key image */ public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { return lastStatuses.get(keyImage); } } public void poll() { - if (daemon == null) { - log.warn("Cannot poll key images because daemon is null"); + if (monerod == null) { + log.warn("Cannot poll key images because monerod is null"); return; } // fetch spent statuses List spentStatuses = null; - List keyImages = new ArrayList(getKeyImages()); + List keyImages = new ArrayList(getNextKeyImageBatch()); try { - spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter + spentStatuses = keyImages.isEmpty() ? new ArrayList() : monerod.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { // limit error logging @@ -252,10 +263,20 @@ public class XmrKeyImagePoller { return; } - // collect changed statuses + // process spent statuses Map changedStatuses = new HashMap(); - synchronized (lastStatuses) { - for (int i = 0; i < spentStatuses.size(); i++) { + synchronized (lock) { + Set allKeyImages = getKeyImages(); + for (int i = 0; i < keyImages.size(); i++) { + + // skip if key image is removed + if (!allKeyImages.contains(keyImages.get(i))) continue; + + // move key image to the end of the queue + keyImagePollQueue.remove(keyImages.get(i)); + keyImagePollQueue.add(keyImages.get(i)); + + // update spent status if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); @@ -265,14 +286,18 @@ public class XmrKeyImagePoller { // announce changes if (!changedStatuses.isEmpty()) { - for (XmrKeyImageListener listener : new ArrayList(listeners)) { + List listeners; + synchronized (lock) { + listeners = new ArrayList(this.listeners); + } + for (XmrKeyImageListener listener : listeners) { listener.onSpentStatusChanged(changedStatuses); } } } private void refreshPolling() { - synchronized (keyImageGroups) { + synchronized (lock) { setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } @@ -291,11 +316,24 @@ public class XmrKeyImagePoller { private Set getKeyImages() { Set allKeyImages = new HashSet(); - synchronized (keyImageGroups) { + synchronized (lock) { for (Set keyImagesGroup : keyImageGroups.values()) { allKeyImages.addAll(keyImagesGroup); } } return allKeyImages; } + + private List getNextKeyImageBatch() { + synchronized (lock) { + List keyImageBatch = new ArrayList<>(); + int count = 0; + for (String keyImage : keyImagePollQueue) { + if (count >= MAX_POLL_SIZE) break; + keyImageBatch.add(keyImage); + count++; + } + return keyImageBatch; + } + } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 9646f5e3f4..7baf9d8fa7 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -6,8 +6,6 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.apache.commons.lang3.exception.ExceptionUtils; - import haveno.common.Timer; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; @@ -28,9 +26,10 @@ import monero.wallet.model.MoneroWalletListener; public abstract class XmrWalletBase { // constants - public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120; + public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 180; public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100; public static final int SAVE_WALLET_DELAY_SECONDS = 300; + private static final String SYNC_PROGRESS_TIMEOUT_MSG = "Sync progress timeout called"; // inherited protected MoneroWallet wallet; @@ -70,75 +69,96 @@ public abstract class XmrWalletBase { public void syncWithProgress(boolean repeatSyncToLatestHeight) { synchronized (walletLock) { + try { - // set initial state - isSyncingWithProgress = true; - syncProgressError = null; - long targetHeightAtStart = xmrConnectionService.getTargetHeight(); - syncStartHeight = walletHeight.get(); - updateSyncProgress(syncStartHeight, targetHeightAtStart); + // set initial state + if (isSyncingWithProgress) log.warn("Syncing with progress while already syncing with progress. That should never happen"); + resetSyncProgressTimeout(); + isSyncingWithProgress = true; + syncProgressError = null; + long targetHeightAtStart = xmrConnectionService.getTargetHeight(); + syncStartHeight = walletHeight.get(); + updateSyncProgress(syncStartHeight, targetHeightAtStart); - // test connection changing on startup before wallet synced - if (testReconnectOnStartup) { - UserThread.runAfter(() -> { - log.warn("Testing connection change on startup before wallet synced"); - if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2); - else xmrConnectionService.setConnection(testReconnectMonerod1); - }, 1); - testReconnectOnStartup = false; // only run once - } + // test connection changing on startup before wallet synced + if (testReconnectOnStartup) { + UserThread.runAfter(() -> { + log.warn("Testing connection change on startup before wallet synced"); + if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2); + else xmrConnectionService.setConnection(testReconnectMonerod1); + }, 1); + testReconnectOnStartup = false; // only run once + } - // native wallet provides sync notifications - if (wallet instanceof MoneroWalletFull) { - if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test - wallet.sync(new MoneroWalletListener() { - @Override - public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { - long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart; - updateSyncProgress(height, appliedTargetHeight); - } - }); - setWalletSyncedWithProgress(); - return; - } - - // start polling wallet for progress - syncProgressLatch = new CountDownLatch(1); - syncProgressLooper = new TaskLooper(() -> { - if (wallet == null) return; - long height; - try { - height = wallet.getHeight(); // can get read timeout while syncing - } catch (Exception e) { - if (wallet != null && !isShutDownStarted) { - log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); - log.warn(ExceptionUtils.getStackTrace(e)); - } - - // stop polling and release latch - syncProgressError = e; - syncProgressLatch.countDown(); + // native wallet provides sync notifications + if (wallet instanceof MoneroWalletFull) { + if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test + wallet.sync(new MoneroWalletListener() { + @Override + public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { + long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart; + updateSyncProgress(height, appliedTargetHeight); + } + }); + setWalletSyncedWithProgress(); return; } - long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart; - updateSyncProgress(height, appliedTargetHeight); - if (height >= appliedTargetHeight) { - setWalletSyncedWithProgress(); - syncProgressLatch.countDown(); + + // start polling wallet for progress + syncProgressLatch = new CountDownLatch(1); + syncProgressLooper = new TaskLooper(() -> { + + // stop if shutdown or null wallet + if (isShutDownStarted || wallet == null) { + syncProgressError = new RuntimeException("Shut down or wallet has become null while syncing with progress"); + syncProgressLatch.countDown(); + return; + } + + // get height + long height; + try { + height = wallet.getHeight(); // can get read timeout while syncing + } catch (Exception e) { + if (wallet != null && !isShutDownStarted) { + log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); + } + if (wallet == null) { + syncProgressError = new RuntimeException("Wallet has become null while syncing with progress"); + syncProgressLatch.countDown(); + } + return; + } + + // update sync progress + long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart; + updateSyncProgress(height, appliedTargetHeight); + if (height >= appliedTargetHeight) { + setWalletSyncedWithProgress(); + syncProgressLatch.countDown(); + } + }); + wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); + syncProgressLooper.start(1000); + + // wait for sync to complete + HavenoUtils.awaitLatch(syncProgressLatch); + + // stop polling + syncProgressLooper.stop(); + syncProgressTimeout.stop(); + if (wallet != null) { // can become null if interrupted by force close + if (syncProgressError == null || !HavenoUtils.isUnresponsive(syncProgressError)) { // TODO: skipping stop sync if unresponsive because wallet will hang. if unresponsive, wallet is assumed to be force restarted by caller, but that should be done internally here instead of externally? + wallet.stopSyncing(); + saveWallet(); + } } - }); - wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - syncProgressLooper.start(1000); - - // wait for sync to complete - HavenoUtils.awaitLatch(syncProgressLatch); - - // stop polling - syncProgressLooper.stop(); - syncProgressTimeout.stop(); - if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close - isSyncingWithProgress = false; - if (syncProgressError != null) throw new RuntimeException(syncProgressError); + if (syncProgressError != null) throw new RuntimeException(syncProgressError); + } catch (Exception e) { + throw e; + } finally { + isSyncingWithProgress = false; + } } } @@ -162,6 +182,10 @@ public abstract class XmrWalletBase { // --------------------------------- ABSTRACT ----------------------------- + public static boolean isSyncWithProgressTimeout(Throwable e) { + return e.getMessage().contains(SYNC_PROGRESS_TIMEOUT_MSG); + } + public abstract void saveWallet(); public abstract void requestSaveWallet(); @@ -171,31 +195,33 @@ public abstract class XmrWalletBase { // ------------------------------ PRIVATE HELPERS ------------------------- private void updateSyncProgress(long height, long targetHeight) { - resetSyncProgressTimeout(); - UserThread.execute(() -> { - // set wallet height - walletHeight.set(height); + // reset progress timeout if height advanced + if (height != walletHeight.get()) { + resetSyncProgressTimeout(); + } - // new wallet reports height 1 before synced - if (height == 1) { - downloadListener.progress(0, targetHeight - height, null); - return; - } + // set wallet height + walletHeight.set(height); - // set progress - long blocksLeft = targetHeight - walletHeight.get(); - if (syncStartHeight == null) syncStartHeight = walletHeight.get(); - double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight)); - downloadListener.progress(percent, blocksLeft, null); - }); + // new wallet reports height 1 before synced + if (height == 1) { + downloadListener.progress(0, targetHeight - height, null); + return; + } + + // set progress + long blocksLeft = targetHeight - height; + if (syncStartHeight == null) syncStartHeight = height; + double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) height - syncStartHeight) / (double) (targetHeight - syncStartHeight)); + downloadListener.progress(percent, blocksLeft, null); } private synchronized void resetSyncProgressTimeout() { if (syncProgressTimeout != null) syncProgressTimeout.stop(); syncProgressTimeout = UserThread.runAfter(() -> { if (isShutDownStarted) return; - syncProgressError = new RuntimeException("Sync progress timeout called"); + syncProgressError = new RuntimeException(SYNC_PROGRESS_TIMEOUT_MSG); syncProgressLatch.countDown(); }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); } @@ -203,6 +229,6 @@ public abstract class XmrWalletBase { private void setWalletSyncedWithProgress() { wasWalletSynced = true; isSyncingWithProgress = false; - syncProgressTimeout.stop(); + if (syncProgressTimeout != null) syncProgressTimeout.stop(); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 8b319622b9..6cb6110011 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -37,6 +37,7 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; +import haveno.core.trade.protocol.TradeProtocol; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.listeners.XmrBalanceListener; @@ -58,11 +59,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.LongProperty; @@ -90,7 +87,6 @@ import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; -import monero.wallet.model.MoneroSyncResult; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxPriority; import monero.wallet.model.MoneroTxQuery; @@ -110,7 +106,6 @@ public class XmrWalletService extends XmrWalletBase { public static final String MONERO_BINS_DIR = Config.appDataDir().getAbsolutePath(); public static final String MONERO_WALLET_RPC_NAME = Utilities.isWindows() ? "monero-wallet-rpc.exe" : "monero-wallet-rpc"; public static final String MONERO_WALLET_RPC_PATH = MONERO_BINS_DIR + File.separator + MONERO_WALLET_RPC_NAME; - public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.DEFAULT; public static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType(); @@ -142,7 +137,6 @@ public class XmrWalletService extends XmrWalletBase { private ChangeListener walletInitListener; private TradeManager tradeManager; - private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private final Object lock = new Object(); private TaskLooper pollLooper; @@ -299,8 +293,8 @@ public class XmrWalletService extends XmrWalletBase { return false; } - public MoneroDaemonRpc getDaemon() { - return xmrConnectionService.getDaemon(); + public MoneroDaemonRpc getMonerod() { + return xmrConnectionService.getMonerod(); } public boolean isProxyApplied() { @@ -308,7 +302,7 @@ public class XmrWalletService extends XmrWalletBase { } public boolean isProxyApplied(boolean wasWalletSynced) { - return preferences.isProxyApplied(wasWalletSynced) && xmrConnectionService.isProxyApplied(); + return xmrConnectionService.isProxyApplied() || preferences.isProxyApplied(wasWalletSynced); } public String getWalletPassword() { @@ -360,23 +354,6 @@ public class XmrWalletService extends XmrWalletBase { return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded(); } - /** - * Sync the given wallet in a thread pool with other wallets. - */ - public MoneroSyncResult syncWallet(MoneroWallet wallet) { - synchronized (HavenoUtils.getDaemonLock()) { // TODO: lock defeats purpose of thread pool - Callable task = () -> { - return wallet.sync(); - }; - Future future = syncWalletThreadPool.submit(task); - try { - return future.get(); - } catch (Exception e) { - throw new MoneroError(e.getMessage()); - } - } - } - public void saveWallet(MoneroWallet wallet) { saveWallet(wallet, false); } @@ -442,6 +419,12 @@ public class XmrWalletService extends XmrWalletBase { if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name); } + public MoneroTxWallet createTx(List destinations) { + MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); + //printTxs("XmrWalletService.createTx", tx); + return tx; + } + public MoneroTxWallet createTx(MoneroTxConfig txConfig) { synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { @@ -456,18 +439,30 @@ public class XmrWalletService extends XmrWalletBase { } } - public String relayTx(String metadata) { + public List createSweepTxs(String address) { + return createSweepTxs(new MoneroTxConfig().setAccountIndex(0).setAddress(address).setRelay(false)); + } + + public List createSweepTxs(MoneroTxConfig txConfig) { synchronized (walletLock) { - String txId = wallet.relayTx(metadata); - requestSaveWallet(); - return txId; + synchronized (HavenoUtils.getWalletFunctionLock()) { + List txs = wallet.sweepUnlocked(txConfig); + if (Boolean.TRUE.equals(txConfig.getRelay())) { + for (MoneroTxWallet tx : txs) cachedTxs.addFirst(tx); + cacheWalletInfo(); + requestSaveWallet(); + } + return txs; + } } } - public MoneroTxWallet createTx(List destinations) { - MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); - //printTxs("XmrWalletService.createTx", tx); - return tx; + public List relayTxs(List metadatas) { + synchronized (walletLock) { + List txIds = wallet.relayTxs(metadatas); + requestSaveWallet(); + return txIds; + } } /** @@ -557,6 +552,7 @@ public class XmrWalletService extends XmrWalletBase { // freeze outputs for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage); + cacheNonPoolTxs(); cacheWalletInfo(); requestSaveWallet(); } @@ -579,11 +575,31 @@ public class XmrWalletService extends XmrWalletBase { // thaw outputs for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage); + cacheNonPoolTxs(); cacheWalletInfo(); requestSaveWallet(); } } + private void cacheNonPoolTxs() { + + // get non-pool txs + List nonPoolTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); + + // replace non-pool txs in cache + for (MoneroTxWallet nonPoolTx : nonPoolTxs) { + boolean replaced = false; + for (int i = 0; i < cachedTxs.size(); i++) { + if (cachedTxs.get(i).getHash().equals(nonPoolTx.getHash())) { + cachedTxs.set(i, nonPoolTx); + replaced = true; + break; + } + } + if (!replaced) cachedTxs.add(nonPoolTx); + } + } + private List getSubaddressesWithExactInput(BigInteger amount) { // fetch unspent, unfrozen, unlocked outputs @@ -737,22 +753,22 @@ public class XmrWalletService extends XmrWalletBase { */ public MoneroTx verifyTradeTx(String offerId, BigInteger tradeFeeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List keyImages) { if (txHash == null) throw new IllegalArgumentException("Cannot verify trade tx with null id"); - MoneroDaemonRpc daemon = getDaemon(); + MoneroDaemonRpc monerod = getMonerod(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; synchronized (lock) { try { // verify tx not submitted to pool - tx = daemon.getTx(txHash); + tx = monerod.getTx(txHash); if (tx != null) throw new RuntimeException("Tx is already submitted"); // submit tx to pool - MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? + MoneroSubmitTxResult result = monerod.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); // get pool tx which has weight and size - for (MoneroTx poolTx : daemon.getTxPool()) if (poolTx.getHash().equals(txHash)) tx = poolTx; + for (MoneroTx poolTx : monerod.getTxPool()) if (poolTx.getHash().equals(txHash)) tx = poolTx; if (tx == null) throw new RuntimeException("Tx is not in pool after being submitted"); // verify key images @@ -767,9 +783,8 @@ public class XmrWalletService extends XmrWalletBase { // verify miner fee BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); - double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); - if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff); - log.info("Trade miner fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); + HavenoUtils.verifyMinerFee(minerFeeEstimate, tx.getFee()); + log.info("Trade miner fee {} is within tolerance", tx.getFee()); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; @@ -808,9 +823,9 @@ public class XmrWalletService extends XmrWalletBase { throw e; } finally { try { - daemon.flushTxPool(txHash); // flush tx from pool + monerod.flushTxPool(txHash); // flush tx from pool } catch (MoneroRpcError err) { - System.out.println(daemon.getRpcConnection()); + System.out.println(monerod.getRpcConnection()); throw err.getCode().equals(-32601) ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; } } @@ -839,7 +854,7 @@ public class XmrWalletService extends XmrWalletBase { } // get fee estimates per kB from daemon - MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); + MoneroFeeEstimate feeEstimates = getMonerod().getFeeEstimate(); BigInteger baseFeeEstimate = feeEstimates.getFees().get(priority.ordinal() - 1); BigInteger qmask = feeEstimates.getQuantizationMask(); log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask); @@ -863,8 +878,8 @@ public class XmrWalletService extends XmrWalletBase { synchronized (txCache) { // fetch txs - if (getDaemon() == null) xmrConnectionService.verifyConnection(); // will throw - List txs = getDaemon().getTxs(txHashes, true); + if (getMonerod() == null) xmrConnectionService.verifyConnection(); // will throw + List txs = getMonerod().getTxs(txHashes, true); // store to cache for (MoneroTx tx : txs) txCache.put(tx.getHash(), Optional.of(tx)); @@ -1039,7 +1054,7 @@ public class XmrWalletService extends XmrWalletBase { // swap trade payout to available if applicable if (tradeManager == null) return; Trade trade = tradeManager.getTrade(offerId); - if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); + if (trade == null || trade.isPayoutFinalized()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } public synchronized void swapPayoutAddressEntryToAvailable(String offerId) { @@ -1183,7 +1198,7 @@ public class XmrWalletService extends XmrWalletBase { Stream available = getFundedAvailableAddressEntries().stream(); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent())); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutFinalized())); return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } @@ -1367,11 +1382,16 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } else { - // force restart main wallet if connection changed while syncing - if (wallet != null) { - log.warn("Force restarting main wallet because connection changed while syncing"); - forceRestartMainWallet(); + // check if ignored + if (wallet == null || isShutDownStarted) return; + if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) { + updatePollPeriod(); + return; } + + // force restart main wallet if connection changed while syncing + log.warn("Force restarting main wallet because connection changed while syncing"); + forceRestartMainWallet(); } }); @@ -1391,10 +1411,10 @@ public class XmrWalletService extends XmrWalletBase { maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } - private void maybeInitMainWallet(boolean sync, int numSyncAttempts) { + private void maybeInitMainWallet(boolean sync, int numSyncAttemptsRemaining) { ThreadUtils.execute(() -> { try { - doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); + doMaybeInitMainWallet(sync, numSyncAttemptsRemaining); } catch (Exception e) { if (isShutDownStarted) return; log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); @@ -1404,14 +1424,14 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } - private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) { + private void doMaybeInitMainWallet(boolean sync, int numSyncAttemptsRemaining) { synchronized (walletLock) { if (isShutDownStarted) return; // open or create wallet main wallet if (wallet == null) { - MoneroDaemonRpc daemon = xmrConnectionService.getDaemon(); - log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); + MoneroDaemonRpc monerod = xmrConnectionService.getMonerod(); + log.info("Initializing main wallet with monerod=" + (monerod == null ? "null" : monerod.getRpcConnection().getUri())); if (walletExists(MONERO_WALLET_NAME)) { wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { @@ -1422,7 +1442,7 @@ public class XmrWalletService extends XmrWalletBase { long date = localDateTime.toEpochSecond(ZoneOffset.UTC); user.setWalletCreationDate(date); } - walletHeight.set(wallet.getHeight()); + if (wallet != null) walletHeight.set(wallet.getHeight()); isClosingWallet = false; } @@ -1432,7 +1452,7 @@ public class XmrWalletService extends XmrWalletBase { // sync main wallet if applicable // TODO: error handling and re-initialization is jenky, refactor - if (sync && numSyncAttempts > 0) { + if (sync && numSyncAttemptsRemaining > 0) { try { // switch connection if disconnected @@ -1442,22 +1462,23 @@ public class XmrWalletService extends XmrWalletBase { } // sync main wallet - log.info("Syncing main wallet"); + log.info("Syncing main wallet from height " + walletHeight.get()); long time = System.currentTimeMillis(); MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { syncWithProgress(true); // repeat sync to latest target height } catch (Exception e) { - log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); + if (wallet != null) log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); forceCloseMainWallet(); requestSwitchToNextBestConnection(sourceConnection); - maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again + maybeInitMainWallet(true, numSyncAttemptsRemaining - 1); // re-initialize wallet and sync again return; } log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); // poll wallet doPollWallet(true); + if (getBalance() == null) throw new RuntimeException("Balance is null after polling main wallet"); if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); // log wallet balances @@ -1485,9 +1506,9 @@ public class XmrWalletService extends XmrWalletBase { saveWallet(false); } catch (Exception e) { if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized - log.warn("Error initially syncing main wallet: {}", e.getMessage()); - if (numSyncAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts); + log.warn("Error initially syncing main wallet, numSyncAttemptsRemaining={}", numSyncAttemptsRemaining, e); + if (numSyncAttemptsRemaining <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing."); HavenoUtils.havenoSetup.getWalletInitialized().set(true); saveWallet(false); @@ -1498,7 +1519,7 @@ public class XmrWalletService extends XmrWalletBase { } else { log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> { - maybeInitMainWallet(true, numSyncAttempts - 1); + maybeInitMainWallet(true, numSyncAttemptsRemaining - 1); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } @@ -1782,7 +1803,7 @@ public class XmrWalletService extends XmrWalletBase { List cmd = new ArrayList<>(Arrays.asList( // modifiable list MONERO_WALLET_RPC_PATH, "--rpc-login", - MONERO_WALLET_RPC_USERNAME + ":" + getWalletPassword(), + MONERO_WALLET_RPC_USERNAME + ":" + MONERO_WALLET_RPC_DEFAULT_PASSWORD, "--wallet-dir", walletDir.toString())); // omit --mainnet flag since it does not exist @@ -1848,8 +1869,8 @@ public class XmrWalletService extends XmrWalletBase { // switch if wallet disconnected if (Boolean.TRUE.equals(connection.isConnected() && !wallet.isConnectedToDaemon())) { - log.warn("Switching to next best connection because main wallet is disconnected"); - if (requestSwitchToNextBestConnection()) return; // calls back to this method + log.warn("Main wallet is disconnected from monerod, requesting switch to next best connection"); + if (requestSwitchToNextBestConnection(connection)) return; // calls back to this method } // update poll period @@ -1922,9 +1943,9 @@ public class XmrWalletService extends XmrWalletBase { doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } - public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { + public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while - requestSwitchToNextBestConnection(sourceConnection); + if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } @@ -1976,7 +1997,7 @@ public class XmrWalletService extends XmrWalletBase { doPollWallet(true); } - private void doPollWallet(boolean updateTxs) { + public void doPollWallet(boolean updateTxs) { // skip if shut down started if (isShutDownStarted) return; @@ -1991,6 +2012,9 @@ public class XmrWalletService extends XmrWalletBase { // poll wallet try { + // skip if shut down started + if (isShutDownStarted) return; + // skip if daemon not synced MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); if (lastInfo == null) { @@ -2000,7 +2024,7 @@ public class XmrWalletService extends XmrWalletBase { if (!xmrConnectionService.isSyncedWithinTolerance()) { // throttle warnings - if (System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) { + if (System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) { log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}, monerod={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight(), xmrConnectionService.getConnection() == null ? null : xmrConnectionService.getConnection().getUri()); lastLogDaemonNotSyncedTimestamp = System.currentTimeMillis(); } @@ -2010,13 +2034,7 @@ public class XmrWalletService extends XmrWalletBase { // sync wallet if behind daemon if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { synchronized (walletLock) { // avoid long sync from blocking other operations - - // TODO: local tests have timing failures unless sync called directly - if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) { - syncMainWallet(); - } else { - syncWithProgress(); - } + syncWithProgress(); } } @@ -2026,6 +2044,7 @@ public class XmrWalletService extends XmrWalletBase { synchronized (walletLock) { // avoid long fetch from blocking other operations synchronized (HavenoUtils.getDaemonLock()) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); + if (lastPollTxsTimestamp == 0) lastPollTxsTimestamp = System.currentTimeMillis(); // set initial timestamp try { cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); lastPollTxsTimestamp = System.currentTimeMillis(); @@ -2036,7 +2055,7 @@ public class XmrWalletService extends XmrWalletBase { if (System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); lastLogPollErrorTimestamp = System.currentTimeMillis(); - if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection); + if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) ThreadUtils.submitToPool(() -> requestSwitchToNextBestConnection(sourceConnection)); } } } @@ -2056,13 +2075,13 @@ public class XmrWalletService extends XmrWalletBase { pollInProgress = false; } } + saveWalletWithDelay(); // cache wallet info last synchronized (walletLock) { if (wallet != null && !isShutDownStarted) { try { cacheWalletInfo(); - saveWalletWithDelay(); } catch (Exception e) { log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } @@ -2071,14 +2090,6 @@ public class XmrWalletService extends XmrWalletBase { } } - private MoneroSyncResult syncMainWallet() { - synchronized (walletLock) { - MoneroSyncResult result = syncWallet(wallet); - walletHeight.set(wallet.getHeight()); - return result; - } - } - public boolean isWalletConnectedToDaemon() { synchronized (walletLock) { try { @@ -2109,6 +2120,7 @@ public class XmrWalletService extends XmrWalletBase { BigInteger unlockedBalance = wallet.getUnlockedBalance(); cachedSubaddresses = wallet.getSubaddresses(0); cachedOutputs = wallet.getOutputs(); + if (cachedTxs == null) cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); // cache and notify changes if (cachedHeight == null) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4a978381ed..a392fc7af2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -115,7 +110,7 @@ shared.belowInPercent=Below % from market price shared.aboveInPercent=Above % from market price shared.enterPercentageValue=Enter % value shared.OR=OR -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Waiting for funds... shared.yourDepositTransactionId=Your deposit transaction ID shared.peerDepositTransactionId=Peer's deposit transaction ID @@ -237,6 +232,7 @@ shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker +shared.none=None #################################################################### @@ -314,9 +310,6 @@ market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades -# OfferBookView -market.offerBook.filterPrompt=Filter - # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from @@ -352,9 +345,9 @@ offerbook.takeOffer=Take offer offerbook.takeOffer.createAccount=Create account and take offer offerbook.takeOffer.enterChallenge=Enter the offer passphrase offerbook.trader=Trader -offerbook.offerersBankId=Maker''s bank ID (BIC/SWIFT): {0} -offerbook.offerersBankName=Maker''s bank name: {0} -offerbook.offerersBankSeat=Maker''s seat of bank country: {0} +offerbook.offerersBankId=Maker's bank ID (BIC/SWIFT): {0} +offerbook.offerersBankName=Maker's bank name: {0} +offerbook.offerersBankSeat=Maker's seat of bank country: {0} offerbook.offerersAcceptedBankSeatsEuro=Accepted seat of bank countries (taker): All Euro countries offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {0} offerbook.availableOffersToBuy=Buy {0} with {1} @@ -400,7 +393,7 @@ offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving add For more information about cloning offers see: [HYPERLINK:https://docs.haveno.exchange/haveno-ui/cloning_an_offer/] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ - {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. + {0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} days offerbook.timeSinceSigning.notSigned.noNeed=N/A @@ -409,8 +402,10 @@ shared.notSigned.noNeedAlts=Cryptocurrency accounts do not feature signing or ag offerbook.nrOffers=No. of offers: {0} offerbook.volume={0} (min - max) +offerbook.volumeTotal={0} {1} offerbook.deposit=Deposit XMR (%) offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed. +offerbook.XMRTotal=XMR ({0}) offerbook.createNewOffer=Create offer to {0} {1} offerbook.createOfferDisabled.tooltip=You can only create one offer at a time @@ -436,8 +431,8 @@ offerbook.warning.newVersionAnnouncement=With this version of the software, trad For more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing]. popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - - The buyer''s account has not been signed by an arbitrator or a peer\n\ - - The time since signing of the buyer''s account is not at least 30 days\n\ + - The buyer's account has not been signed by an arbitrator or a peer\n\ + - The time since signing of the buyer's account is not at least 30 days\n\ - The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\ - Your account has not been signed by an arbitrator or a peer\n\ @@ -457,7 +452,7 @@ offerbook.warning.requireUpdateToNewVersion=Your version of Haveno is not compat offerbook.warning.offerWasAlreadyUsedInTrade=You cannot take this offer because you already took it earlier. \ It could be that your previous take-offer attempt resulted in a failed trade. -offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is invalid. +offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is not registered. offerbook.warning.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid. offerbook.warning.reserveFundsSpent=This offer cannot be taken because the reserved funds were already spent. @@ -479,12 +474,8 @@ createOffer.amount.prompt=Enter amount in XMR createOffer.price.prompt=Enter price createOffer.volume.prompt=Enter amount in {0} createOffer.amountPriceBox.amountDescription=Amount of XMR to {0} -createOffer.amountPriceBox.buy.amountDescriptionCrypto=Amount of XMR to sell -createOffer.amountPriceBox.sell.amountDescriptionCrypto=Amount of XMR to buy createOffer.amountPriceBox.buy.volumeDescription=Amount in {0} to spend createOffer.amountPriceBox.sell.volumeDescription=Amount in {0} to receive -createOffer.amountPriceBox.buy.volumeDescriptionCrypto=Amount in {0} to sell -createOffer.amountPriceBox.sell.volumeDescriptionCrypto=Amount in {0} to buy createOffer.amountPriceBox.minAmountDescription=Minimum amount of XMR createOffer.securityDeposit.prompt=Security deposit createOffer.fundsBox.title=Fund your offer @@ -513,8 +504,8 @@ createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters. # new entries -createOffer.placeOfferButton=Review: Place offer to {0} monero -createOffer.placeOfferButtonCrypto=Review: Place offer to {0} {1} +createOffer.placeOfferButton.buy=Review: Place offer to buy XMR with {0} +createOffer.placeOfferButton.sell=Review: Place offer to sell XMR for {0} createOffer.createOfferFundWalletInfo.headline=Fund your offer # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n @@ -546,7 +537,7 @@ createOffer.tac=With publishing this offer I agree to trade with any trader who createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) -createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=Minimum security deposit is used createOffer.buyerAsTakerWithoutDeposit=No deposit required from buyer (passphrase protected) @@ -584,8 +575,8 @@ takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open takeOffer.error.message=An error occurred when taking the offer.\n\n{0} # new entries -takeOffer.takeOfferButton=Review: Take offer to {0} monero -takeOffer.takeOfferButtonCrypto=Review: Take offer to {0} {1} +takeOffer.takeOfferButton.buy=Review: Take offer to buy XMR with {0} +takeOffer.takeOfferButton.sell=Review: Take offer to sell XMR for {0} takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentage price based on the market price but there is no price feed available. takeOffer.takeOfferFundWalletInfo.headline=Fund your trade # suppress inspection "TrailingSpacesInProperty" @@ -669,6 +660,7 @@ portfolio.pending.unconfirmedTooLong=Deposit transactions on trade {0} are still If the problem persists, contact Haveno support [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.step1.waitForConf=Wait for blockchain confirmations +portfolio.pending.step2_buyer.additionalConf=Deposits have reached 10 confirmations.\nFor extra security, we recommend waiting {0} confirmations before sending payment.\nProceed early at your own risk. portfolio.pending.step2_buyer.startPayment=Start payment portfolio.pending.step2_seller.waitPaymentSent=Wait until payment has been sent portfolio.pending.step3_buyer.waitPaymentArrived=Wait until payment arrived @@ -730,11 +722,11 @@ portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the XMR seller by using MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}. + The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the XMR seller by using Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}. + The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the XMR seller.\n\n @@ -743,13 +735,13 @@ portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. \ See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the XMR seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer -portfolio.pending.step2_buyer.sellersAddress=Seller''s {0} address +portfolio.pending.step2_buyer.sellersAddress=Seller's {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used portfolio.pending.step2_buyer.paymentSent=Payment sent portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. @@ -761,15 +753,15 @@ portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\n\ Then tear it in 2 parts, make a photo and send it to the XMR seller's email address. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Send Authorisation number and receipt portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=You need to send the Authorisation number and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}.\n\n\ + The receipt must clearly show the seller's full name, country, state and the amount. The seller's email is: {0}.\n\n\ Did you send the Authorisation number and contract to the seller? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Send MTCN and receipt portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=You need to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller.\n\ - The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}.\n\n\ + The receipt must clearly show the seller's full name, city, country and the amount. The seller's email is: {0}.\n\n\ Did you send the MTCN and contract to the seller? portfolio.pending.step2_buyer.halCashInfo.headline=Send HalCash code portfolio.pending.step2_buyer.halCashInfo.msg=You need to send a text message with the HalCash code as well as the \ - trade ID ({0}) to the XMR seller.\nThe seller''s mobile nr. is {1}.\n\n\ + trade ID ({0}) to the XMR seller.\nThe seller's mobile nr. is {1}.\n\n\ Did you send the code to the seller? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Some banks might verify the receiver's name. \ Faster Payments accounts created in old Haveno clients do not provide the receiver's name, \ @@ -789,8 +781,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Buyer's contact information portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction is unlocked.\nYou need to wait until the XMR buyer starts the {0} payment. portfolio.pending.step2_seller.warn=The XMR buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for assistance. -disputeChat.chatWindowTitle=Dispute chat window for trade with ID ''{0}'' -tradeChat.chatWindowTitle=Trader Chat window for trade with ID ''{0}'' +disputeChat.chatWindowTitle=Dispute chat window for trade with ID '{0}' +tradeChat.chatWindowTitle=Trader Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ It is not mandatory to reply in the chat.\n\ @@ -818,7 +810,7 @@ message.state.ACKNOWLEDGED=Peer confirmed message receipt message.state.FAILED=Sending message failed portfolio.pending.step3_buyer.wait.headline=Wait for XMR seller's payment confirmation -portfolio.pending.step3_buyer.wait.info=Waiting for the XMR seller''s confirmation for the receipt of the {0} payment. +portfolio.pending.step3_buyer.wait.info=Waiting for the XMR seller's confirmation for the receipt of the {0} payment. portfolio.pending.step3_buyer.wait.msgStateInfo.label=Payment started message status portfolio.pending.step3_buyer.warn.part1a=on the {0} blockchain portfolio.pending.step3_buyer.warn.part1b=at your payment provider (e.g. bank) @@ -857,7 +849,7 @@ portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon e message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted \ confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\n\ +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\n\ If the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n @@ -882,7 +874,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirm that you have received the payment @@ -945,6 +937,8 @@ portfolio.pending.support.headline.getHelp=Need help? portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over +portfolio.pending.support.headline.depositTxMissing=Missing deposit transaction +portfolio.pending.support.depositTxMissing=A deposit transaction is missing for this trade. Open a support ticket to contact an arbitrator for assistance. portfolio.pending.arbitrationRequested=Arbitration requested portfolio.pending.mediationRequested=Mediation requested @@ -963,7 +957,7 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\n\ You receive: {0}\n\ Your trading peer receives: {1}\n\n\ @@ -972,12 +966,12 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\ If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \ second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ - The arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. \ - Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \ + The arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. \ + Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for \ exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \ (or if the other peer is unresponsive).\n\n\ More details about the new arbitration model: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout \ but it seems that your trading peer has not accepted it.\n\n\ Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ investigate the case again and do a payout based on their findings.\n\n\ @@ -993,11 +987,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee trans Without this tx, the trade cannot be completed. No funds have been locked. \ Your offer is still available to other traders, so you have not lost the maker fee. \ You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\n\ - Without this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. \ - You can make a request to be reimbursed the trade fee here: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Feel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=A deposit transaction is missing.\n\nThis transaction is required to complete the trade. Please ensure your wallet is fully synchronized with the Monero blockchain.\n\nYou can move this trade to the "Failed Trades" section to deactivate it. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ Please do NOT send the traditional or cryptocurrency payment to the XMR seller, because without the delayed payout tx, arbitration \ @@ -1137,6 +1127,8 @@ funds.tx.disputeLost=Lost dispute case: {0} funds.tx.collateralForRefund=Refund collateral: {0} funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} funds.tx.refund=Refund from arbitration: {0} +funds.tx.makerTradeFee=Maker fee: {0} +funds.tx.takerTradeFee=Taker fee: {0} funds.tx.unknown=Unknown reason: {0} funds.tx.noFundsFromDispute=No refund from dispute funds.tx.receivedFunds=Received funds @@ -1163,8 +1155,6 @@ support.tab.refund.support=Refund support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.tab.SignedOffers=Signed Offers support.prompt.signedOffer.penalty.msg=This will charge the maker a penalty fee and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\ Offer ID: {0}\n\ @@ -1275,7 +1265,7 @@ support.initialInfo=Please enter a description of your problem in the text field \t Sometimes the data directory gets corrupted and leads to strange bugs. \n\ \t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Please make yourself familiar with the basic rules for the dispute process:\n\ -\t● You need to respond to the {0}''s requests within 2 days.\n\ +\t● You need to respond to the {0}'s requests within 2 days.\n\ \t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\ \t● The maximum period for a dispute is 14 days.\n\ \t● You need to cooperate with the {1} and provide the information they request to make your case.\n\ @@ -1288,9 +1278,9 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorReceivedLogs=System message: Mediator has received logs: {0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\ @@ -1338,6 +1328,7 @@ setting.preferences.displayOptions=Display options setting.preferences.showOwnOffers=Show my own offers in offer book setting.preferences.useAnimations=Use animations setting.preferences.useDarkMode=Use dark mode +setting.preferences.useLightMode=Use light mode setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1345,6 +1336,7 @@ setting.preferences.notifyOnPreRelease=Receive pre-release notifications setting.preferences.resetAllFlags=Reset all \"Don't show again\" flags settings.preferences.languageChange=To apply the language change to all screens requires a restart. settings.preferences.supportLanguageWarning=In case of a dispute, please note that arbitration is handled in {0}. +setting.preferences.clearDataAfterDays=Clear sensitive data after (days) settings.preferences.editCustomExplorer.headline=Explorer Settings settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ customize to suit your own preferences. @@ -1354,6 +1346,15 @@ settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=New data-privacy feature +settings.preferences.sensitiveDataRemoval.msg=To protect the privacy of yourself and other traders, Haveno intends to \ + remove sensitive data from old trades. This is particularly important for fiat trades which may include bank \ + account details.\n\n\ + The threshold for data removal can be configured on this screen via the field "Clear sensitive data after (days)". \ + It is recommended to set it as low as possible, for example 60 days. That means trades from more than 60 \ + days ago will have sensitive data cleared, as long as they are completed. Completed trades are found in the \ + Portfolio / History tab. + settings.net.xmrHeader=Monero network settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=My onion address @@ -1431,19 +1432,19 @@ setting.about.subsystems.label=Versions of subsystems setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Local DB version: {2}; Trade protocol version: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1521,8 +1522,8 @@ account.arbitratorRegistration.registerFailed=Could not complete registration.{0 account.crypto.yourCryptoAccounts=Your cryptocurrency accounts account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as \ -described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or \ -(b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ +described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or \ +(b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=I understand and confirm that I know which wallet I need to use. # suppress inspection "UnusedProperty" @@ -1827,8 +1828,8 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Offer type account.notifications.marketAlert.message.title=Offer alert account.notifications.marketAlert.message.msg.below=below account.notifications.marketAlert.message.msg.above=above -account.notifications.marketAlert.message.msg=A new ''{0} {1}'' offer with price {2} ({3} {4} market price) and \ - payment method ''{5}'' was published to the Haveno offerbook.\n\ +account.notifications.marketAlert.message.msg=A new '{0} {1}' offer with price {2} ({3} {4} market price) and \ + payment method '{5}' was published to the Haveno offerbook.\n\ Offer ID: {6}. account.notifications.priceAlert.message.title=Price alert for {0} account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2} @@ -2006,13 +2007,14 @@ offerDetailsWindow.countryBank=Maker's country of bank offerDetailsWindow.commitment=Commitment offerDetailsWindow.agree=I agree offerDetailsWindow.tac=Terms and conditions -offerDetailsWindow.confirm.maker=Confirm: Place offer to {0} monero -offerDetailsWindow.confirm.makerCrypto=Confirm: Place offer to {0} {1} -offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} monero -offerDetailsWindow.confirm.takerCrypto=Confirm: Take offer to {0} {1} +offerDetailsWindow.confirm.maker.buy=Confirm: Place offer to buy XMR with {0} +offerDetailsWindow.confirm.maker.sell=Confirm: Place offer to sell XMR for {0} +offerDetailsWindow.confirm.taker.buy=Confirm: Take offer to buy XMR with {0} +offerDetailsWindow.confirm.taker.sell=Confirm: Take offer to sell XMR for {0} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address offerDetailsWindow.challenge=Offer passphrase +offerDetailsWindow.challenge.copy=Copy passphrase to share with your peer qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -2090,7 +2092,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun walletPasswordWindow.headline=Enter password to unlock xmrConnectionError.headline=Monero connection error -xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.providedNodes=Error connecting to provided Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.customNodes=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. xmrConnectionError.localNode.start=Start local node xmrConnectionError.localNode.start.error=Error starting local node @@ -2116,7 +2119,7 @@ torNetworkSettingWindow.deleteFiles.progress=Shut down Tor in progress torNetworkSettingWindow.deleteFiles.success=Outdated Tor files deleted successfully. Please restart. torNetworkSettingWindow.bridges.header=Is Tor blocked? torNetworkSettingWindow.bridges.info=If Tor is blocked by your internet provider or by your country you can try to use Tor bridges.\n\ - Visit the Tor web page at: https://bridges.torproject.org/bridges to learn more about \ + Visit the Tor web page at: https://bridges.torproject.org to learn more about \ bridges and pluggable transports. feeOptionWindow.useXMR=Use XMR @@ -2159,7 +2162,7 @@ error.closedTradeWithNoDepositTx=The deposit transaction of the closed trade wit popup.warning.walletNotInitialized=The wallet is not initialized yet popup.warning.wrongVersion=You probably have the wrong Haveno version for this computer.\n\ -Your computer''s architecture is: {0}.\n\ +Your computer's architecture is: {0}.\n\ The Haveno binary you installed is: {1}.\n\ Please shut down and re-install the correct version ({2}). popup.warning.incompatibleDB=We detected incompatible data base files!\n\n\ @@ -2194,6 +2197,7 @@ popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\n\ Deposit tx address: {1}\n\ Trade ID: {2}.\n\n\ Please open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." +popup.warning.moneroConnection=There was a problem connecting to the Monero network.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -2209,7 +2213,7 @@ popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version Please check out the Haveno Forum for more information. popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the network administrators to register a filter object. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. \ - Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. + Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\n\ Transaction ID={1}.\n\ @@ -2231,7 +2235,7 @@ popup.warning.openOfferWithInvalidMakerFeeTx=The maker fee transaction for offer For further help please contact the Haveno support channel at the Haveno Keybase team. popup.info.cashDepositInfo=Please be sure that you have a bank branch in your area to be able to make the cash deposit.\n\ - The bank ID (BIC/SWIFT) of the seller''s bank is: {0}. + The bank ID (BIC/SWIFT) of the seller's bank is: {0}. popup.info.cashDepositInfo.confirm=I confirm that I can make the deposit popup.info.shutDownWithOpenOffers=Haveno is being shut down, but there are open offers. \n\n\ These offers won't be available on the P2P network while Haveno is shut down, but \ @@ -2299,8 +2303,8 @@ popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\n\ For further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \ and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -2337,6 +2341,7 @@ notification.ticket.headline=Support ticket for trade with ID {0} notification.trade.completed=The trade is now completed, and you can withdraw your funds. notification.trade.accepted=Your offer has been accepted by a XMR {0}. notification.trade.unlocked=Your trade has been confirmed.\nYou can start the payment now. +notification.trade.finalized=The trade has {0} confirmations.\nYou can start the payment now. notification.trade.paymentSent=The XMR buyer has sent the payment. notification.trade.selectTrade=Select trade notification.trade.peerOpenedDispute=Your trading peer has opened a {0}. @@ -2682,13 +2687,13 @@ payment.zelle.info=Zelle is a money transfer service that works best *through* a 3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n\ 4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\n\ If you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\n\ - Because of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ + Because of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer \ really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster \ +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster \ Payments transfers. Your current Faster Payments account does not specify a full name.\n\n\ Please consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\n\ When you recreate the account, make sure to copy the precise sort code, account number and account age verification \ - salt values from your old account to your new account. This will ensure your existing account''s age and signing \ + salt values from your old account to your new account. This will ensure your existing account's age and signing \ status are preserved. payment.fasterPayments.ukSortCode="UK sort code" payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. \ @@ -2737,8 +2742,8 @@ payment.cashDeposit.info=Please confirm your bank allows you to send cash deposi payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. payment.account.revolut.addUserNameInfo={0}\n\ - Your existing Revolut account ({1}) does not have a ''Username''.\n\ - Please enter your Revolut ''Username'' to update your account data.\n\ + Your existing Revolut account ({1}) does not have a 'Username'.\n\ + Please enter your Revolut 'Username' to update your account data.\n\ This will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account @@ -3104,7 +3109,7 @@ payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\n\ Three important notes:\n\ - try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n\ - - try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ + - try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat \ to tell your trading peer the reference text you picked so they can verify your payment)\n\ - Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=For your protection, we strongly discourage using Paysafecard PINs for payment.\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 16b6a05bba..17913734f9 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -315,7 +310,6 @@ market.tabs.spreadPayment=Nabídky podle způsobů platby market.tabs.trades=Obchody # OfferBookView -market.offerBook.filterPrompt=Filtr # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu @@ -442,7 +436,7 @@ offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompat offerbook.warning.offerWasAlreadyUsedInTrade=Tuto nabídku nemůžete přijmout, protože jste ji již dříve využili. \ Je možné, že váš předchozí pokus o přijetí nabídky vyústil v neúspěšný obchod. -offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože rozhodce je neplatný +offerbook.warning.arbitratorNotValidated=Tuto nabídku nelze přijmout, protože arbitr není registrován. offerbook.warning.signatureNotValidated=Tuto nabídku nelze přijmout, protože rozhodce má neplatný podpis offerbook.info.sellAtMarketPrice=Budete prodávat za tržní cenu (aktualizováno každou minutu). @@ -463,12 +457,8 @@ createOffer.amount.prompt=Zadejte množství v XMR createOffer.price.prompt=Zadejte cenu createOffer.volume.prompt=Zadejte množství v {0} createOffer.amountPriceBox.amountDescription=Množství XMR, které chcete {0} -createOffer.amountPriceBox.buy.amountDescriptionCrypto=Množství XMR k prodeji -createOffer.amountPriceBox.sell.amountDescriptionCrypto=Množství XMR k nákupu createOffer.amountPriceBox.buy.volumeDescription=Množství {0}, které odešlete createOffer.amountPriceBox.sell.volumeDescription=Množství {0}, které přijmete -createOffer.amountPriceBox.buy.volumeDescriptionCrypto=Množství {0} k prodání -createOffer.amountPriceBox.sell.volumeDescriptionCrypto=Množství {0} k nakoupení createOffer.amountPriceBox.minAmountDescription=Minimální množství XMR createOffer.securityDeposit.prompt=Kauce createOffer.fundsBox.title=Financovat nabídku @@ -496,8 +486,8 @@ createOffer.triggerPrice.invalid.tooLow=Hodnota musí být vyšší než {0} createOffer.triggerPrice.invalid.tooHigh=Hodnota musí být nižší než {0} # new entries -createOffer.placeOfferButton=Zkontrolovat vytvoření nabídky {0} monero -createOffer.placeOfferButtonCrypto=Zkontrolovat vytvoření nabídky {0} {1} +createOffer.placeOfferButton.buy=Zkontrolovat: Vytvořit nabídku na nákup XMR za {0} +createOffer.placeOfferButton.sell=Zkontrolovat: Vytvořit nabídku na prodej XMR za {0} createOffer.createOfferFundWalletInfo.headline=Financovat nabídku # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0}\n @@ -567,8 +557,8 @@ takeOffer.success.info=Stav vašeho obchodu můžete vidět v \"Portfolio/Otevř takeOffer.error.message=Při převzetí nabídky došlo k chybě.\n\n{0} # new entries -takeOffer.takeOfferButton=Zkontrolovat přijetí nabídky {0} monero -takeOffer.takeOfferButtonCrypto=Zkontrolovat přijetí nabídky {0} {1} +takeOffer.takeOfferButton.buy=Zkontrolovat: Přijmout nabídku na nákup XMR za {0} +takeOffer.takeOfferButton.sell=Zkontrolovat: Přijmout nabídku na prodej XMR za {0} takeOffer.noPriceFeedAvailable=Tuto nabídku nemůžete vzít, protože používá procentuální cenu založenou na tržní ceně, ale není k dispozici žádný zdroj cen. takeOffer.takeOfferFundWalletInfo.headline=Financovat obchod # suppress inspection "TrailingSpacesInProperty" @@ -635,6 +625,7 @@ portfolio.pending.unconfirmedTooLong=Vkladové transakce obchodu {0} jsou stále Pokud problém přetrvává, kontaktujte podporu Haveno [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.step1.waitForConf=Počkejte na potvrzení na blockchainu +portfolio.pending.step2_buyer.additionalConf=Vklady dosáhly 10 potvrzení.\nPro vyšší bezpečnost doporučujeme počkat na {0} potvrzení před odesláním platby.\nPokračujte dříve na vlastní riziko. portfolio.pending.step2_buyer.startPayment=Zahajte platbu portfolio.pending.step2_seller.waitPaymentSent=Počkejte, než začne platba portfolio.pending.step3_buyer.waitPaymentArrived=Počkejte, než dorazí platba @@ -755,8 +746,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Kontaktní informace kupujícíh portfolio.pending.step2_seller.waitPayment.msg=Vkladová transakce má alespoň jedno potvrzení na blockchainu.\nMusíte počkat, než kupující XMR zahájí platbu {0}. portfolio.pending.step2_seller.warn=Kupující XMR dosud neprovedl platbu {0}.\nMusíte počkat, než zahájí platbu.\nPokud obchod nebyl dokončen dne {1}, bude rozhodce vyšetřovat. portfolio.pending.step2_seller.openForDispute=Kupující XMR ještě nezačal s platbou!\nMax. povolené období pro obchod vypršelo.\nMůžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. -disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID ''{0}'' -tradeChat.chatWindowTitle=Okno chatu pro obchod s ID ''{0}'' +disputeChat.chatWindowTitle=Okno chatu sporu pro obchod s ID '{0}' +tradeChat.chatWindowTitle=Okno chatu pro obchod s ID '{0}' tradeChat.openChat=Otevřít chatovací okno tradeChat.rules=Můžete komunikovat se svým obchodním partnerem a vyřešit případné problémy s tímto obchodem.\n\ Odpovídat v chatu není povinné.\n\ @@ -911,6 +902,8 @@ portfolio.pending.support.headline.getHelp=Potřebujete pomoc? portfolio.pending.support.button.getHelp=Otevřít obchodní chat portfolio.pending.support.headline.halfPeriodOver=Zkontrolujte platbu portfolio.pending.support.headline.periodOver=Obchodní období skončilo +portfolio.pending.support.headline.depositTxMissing=Chybějící vkladová transakce +portfolio.pending.support.depositTxMissing=U tohoto obchodu chybí transakce vkladu. Otevřete podporu, abyste kontaktovali rozhodce a získali pomoc. portfolio.pending.arbitrationRequested=Požádáno o arbitráž portfolio.pending.mediationRequested=Požádáno o mediaci @@ -959,11 +952,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky. Vaše nabídka je \ stále k dispozici dalším obchodníkům, takže jste neztratili poplatek za vytvoření. \ Tento obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\n\ - Bez této tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. \ - Zde můžete požádat o vrácení obchodního poplatku: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Klidně můžete přesunout tento obchod do neúspěšných obchodů. +portfolio.pending.failedTrade.missingDepositTx=Chybí vkladová transakce.\n\nTato transakce je nutná k dokončení obchodu. Ujistěte se, že je vaše peněženka plně synchronizována s blockchainem Monero.\n\nTento obchod můžete přesunout do sekce „Neúspěšné obchody“ pro jeho deaktivaci. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\n\ Nezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. \ @@ -1129,8 +1118,6 @@ support.tab.refund.support=Vrácení peněz support.tab.arbitration.support=Arbitráž support.tab.legacyArbitration.support=Starší arbitráž support.tab.ArbitratorsSupportTickets=Úkoly pro {0} -support.filter=Hledat spory -support.filter.prompt=Zadejte ID obchodu, datum, onion adresu nebo údaje o účtu support.tab.SignedOffers=Podepsané nabídky support.prompt.signedOffer.penalty.msg=Tím se tvůrci účtuje sankční poplatek a zbývající prostředky z obchodu se vrátí do jeho peněženky. Jste si jisti, že chcete odeslat?\n\n\ ID nabídky: {0}\n\ @@ -1304,6 +1291,7 @@ setting.preferences.displayOptions=Zobrazit možnosti setting.preferences.showOwnOffers=Zobrazit mé vlastní nabídky v seznamu nabídek setting.preferences.useAnimations=Použít animace setting.preferences.useDarkMode=Použít tmavý režim +setting.preferences.useLightMode=Použijte světlý režim setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/obchodů setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované způsoby platby setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API @@ -1320,6 +1308,9 @@ settings.preferences.editCustomExplorer.name=Jméno settings.preferences.editCustomExplorer.txUrl=Transakční URL settings.preferences.editCustomExplorer.addressUrl=Adresa URL +setting.info.headline=Nová funkce ochrany osobních údajů +settings.preferences.sensitiveDataRemoval.msg=Aby byla chráněna vaše soukromí i soukromí ostatních obchodníků, Haveno zamýšlí odstranit citlivá data ze starých obchodů.\n\nDoporučuje se nastavit tento limit co nejníže, například na 60 dní. To znamená, že obchody starší než 60 dní budou mít citlivá data odstraněna, pokud jsou dokončené. Dokončené obchody najdete na záložce Portfolio / Historie. + settings.net.xmrHeader=Síť Monero settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa @@ -1397,19 +1388,19 @@ setting.about.subsystems.label=Verze subsystémů setting.about.subsystems.val=Verze sítě: {0}; Verze zpráv P2P: {1}; Verze lokální DB: {2}; Verze obchodního protokolu: {3} setting.about.shortcuts=Zkratky -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' nebo ''alt + {0}'' nebo ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' nebo 'alt + {0}' nebo 'cmd + {0}' setting.about.shortcuts.menuNav=Procházet hlavní nabídku setting.about.shortcuts.menuNav.value=Pro pohyb v hlavním menu stiskněte: 'Ctrl' nebo 'alt' nebo 'cmd' s numerickou klávesou mezi '1-9' setting.about.shortcuts.close=Zavřít Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' nebo ''cmd + {0}'' nebo ''Ctrl + {1}'' nebo ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' nebo 'cmd + {0}' nebo 'Ctrl + {1}' nebo 'cmd + {1}' setting.about.shortcuts.closePopup=Zavřete vyskakovací nebo dialogové okno setting.about.shortcuts.closePopup.value=Klávesa 'ESCAPE' setting.about.shortcuts.chatSendMsg=Odeslat obchodní soukromou zprávu -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' nebo ''alt + ENTER'' nebo ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' nebo 'alt + ENTER' nebo 'cmd + ENTER' setting.about.shortcuts.openDispute=Otevřít spor setting.about.shortcuts.openDispute.value=Vyberte nevyřízený obchod a klikněte na: {0} @@ -1793,8 +1784,8 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Typ nabídky account.notifications.marketAlert.message.title=Upozornění na nabídku account.notifications.marketAlert.message.msg.below=pod account.notifications.marketAlert.message.msg.above=nad -account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka ''{0} {1}'' s cenou {2} ({3} {4} tržní cena) a \ - způsob platby ''{5}''.\n\ +account.notifications.marketAlert.message.msg=Do Haveno byla zveřejněna nová nabídka '{0} {1}' s cenou {2} ({3} {4} tržní cena) a \ + způsob platby '{5}'.\n\ ID nabídky: {6}. account.notifications.priceAlert.message.title=Upozornění na cenu pro {0} account.notifications.priceAlert.message.msg=Vaše upozornění na cenu bylo aktivováno. Aktuální {0} cena je {1} {2} @@ -1972,13 +1963,14 @@ offerDetailsWindow.countryBank=Země původu banky tvůrce offerDetailsWindow.commitment=Závazek offerDetailsWindow.agree=Souhlasím offerDetailsWindow.tac=Pravidla a podmínky -offerDetailsWindow.confirm.maker=Potvrďte: Přidat nabídku {0} monero -offerDetailsWindow.confirm.makerCrypto=Potvrďte: Přidat nabídku do {0} {1} -offerDetailsWindow.confirm.taker=Potvrďte: Přijmout nabídku {0} monero -offerDetailsWindow.confirm.takerCrypto=Potvrďte: Přijmout nabídku {0} {1} +offerDetailsWindow.confirm.maker.buy=Potvrdit: Vytvořit nabídku na nákup XMR za {0} +offerDetailsWindow.confirm.maker.sell=Potvrdit: Vytvořit nabídku na prodej XMR za {0} +offerDetailsWindow.confirm.taker.buy=Potvrdit: Přijmout nabídku na nákup XMR za {0} +offerDetailsWindow.confirm.taker.sell=Potvrdit: Přijmout nabídku na prodej XMR za {0} offerDetailsWindow.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce offerDetailsWindow.challenge=Passphrase nabídky +offerDetailsWindow.challenge.copy=Zkopírujte přístupovou frázi pro sdílení s protějškem qRCodeWindow.headline=QR Kód qRCodeWindow.msg=Použijte tento QR kód k financování vaší peněženky Haveno z vaší externí peněženky. @@ -2078,7 +2070,7 @@ torNetworkSettingWindow.deleteFiles.progress=Probíhá vypínání sítě Tor torNetworkSettingWindow.deleteFiles.success=Zastaralé soubory Tor byly úspěšně odstraněny. Prosím restartujte aplikaci. torNetworkSettingWindow.bridges.header=Je Tor blokovaný? torNetworkSettingWindow.bridges.info=Pokud je Tor zablokován vaším internetovým poskytovatelem nebo vaší zemí, můžete zkusit použít Tor mosty (bridges).\n\ -Navštivte webovou stránku Tor na adrese: https://bridges.torproject.org/bridges, \ +Navštivte webovou stránku Tor na adrese: https://bridges.torproject.org, \ kde se dozvíte více o mostech a pluggable transports. feeOptionWindow.useXMR=Použít XMR @@ -2156,6 +2148,7 @@ popup.warning.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného Adresa vkladové tx: {1}\n\ ID obchodu: {2}.\n\n\ Otevřete prosím úkol pro podporu výběrem obchodu na obrazovce otevřených obchodů a stisknutím \"alt + o\" nebo \"option + o\"." +popup.warning.moneroConnection=Došlo k problému s připojením k síti Monero.\n\n{0} popup.warning.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n takeOffer.cancelButton=Zrušit akceptaci nabídky diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 4663384a66..801d322bf4 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=Wert muss höher sein als {0} createOffer.triggerPrice.invalid.tooHigh=Wert muss niedriger sein als {0} # new entries -createOffer.placeOfferButton=Überprüfung: Anbieten moneros zu {0} +createOffer.placeOfferButton.buy=Überprüfen: Angebot zum Kauf von XMR mit {0} erstellen +createOffer.placeOfferButton.sell=Überprüfen: Angebot zum Verkauf von XMR für {0} erstellen createOffer.createOfferFundWalletInfo.headline=Ihr Angebot finanzieren # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0} \n @@ -525,7 +521,8 @@ takeOffer.success.info=Sie können den Status Ihres Trades unter \"Portfolio/Off takeOffer.error.message=Bei der Angebotsannahme trat ein Fehler auf.\n\n{0} # new entries -takeOffer.takeOfferButton=Überprüfung: Angebot annehmen moneros zu {0} +takeOffer.takeOfferButton.buy=Überprüfen: Angebot zum Kauf von XMR mit {0} annehmen +takeOffer.takeOfferButton.sell=Überprüfen: Angebot zum Verkauf von XMR für {0} annehmen takeOffer.noPriceFeedAvailable=Sie können dieses Angebot nicht annehmen, da es auf einem Prozentsatz vom Marktpreis basiert, jedoch keiner verfügbar ist. takeOffer.takeOfferFundWalletInfo.headline=Ihren Handel finanzieren # suppress inspection "TrailingSpacesInProperty" @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=Prozentuale Preisabweichung vom Markt portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Auf Blockchain-Bestätigung warten +portfolio.pending.step2_buyer.additionalConf=Einzahlungen haben 10 Bestätigungen erreicht.\nFür zusätzliche Sicherheit empfehlen wir, {0} Bestätigungen abzuwarten, bevor Sie die Zahlung senden.\nEin früheres Vorgehen erfolgt auf eigenes Risiko. portfolio.pending.step2_buyer.startPayment=Zahlung beginnen portfolio.pending.step2_seller.waitPaymentSent=Auf Zahlungsbeginn warten portfolio.pending.step3_buyer.waitPaymentArrived=Auf Zahlungseingang warten @@ -676,7 +674,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Kontaktinformation des Käufers portfolio.pending.step2_seller.waitPayment.msg=Die Kautionstransaktion hat mindestens eine Blockchain-Bestätigung.\nSie müssen warten bis der XMR-Käufer die {0}-Zahlung beginnt. portfolio.pending.step2_seller.warn=Der XMR-Käufer hat die {0}-Zahlung noch nicht getätigt.\nSie müssen warten bis die Zahlung begonnen wurde.\nWenn der Handel nicht bis {1} abgeschlossen wurde, wird der Vermittler diesen untersuchen. portfolio.pending.step2_seller.openForDispute=Der XMR-Käufer hat seine Zahlung nicht begonnen!\nDie maximal zulässige Frist für den Handel ist abgelaufen.\nSie können länger warten und dem Handelspartner mehr Zeit geben oder den Vermittler um Hilfe bitten. -tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID ''{0}'' +tradeChat.chatWindowTitle=Chat-Fenster für Trade mit ID '{0}' tradeChat.openChat=Chat-Fenster öffnen tradeChat.rules=Sie können mit Ihrem Trade-Partner kommunizieren, um mögliche Probleme mit diesem Trade zu lösen.\nEs ist nicht zwingend erforderlich, im Chat zu antworten.\nWenn ein Trader gegen eine der folgenden Regeln verstößt, eröffnen Sie einen Streitfall und melden Sie ihn dem Mediator oder Vermittler.\n\nChat-Regeln:\n\t● Senden Sie keine Links (Risiko von Malware). Sie können die Transaktions-ID und den Namen eines Block-Explorers senden.\n\t● Senden Sie keine Seed-Wörter, Private Keys, Passwörter oder andere sensible Informationen!\n\t● Traden Sie nicht außerhalb von Haveno (keine Sicherheit).\n\t● Beteiligen Sie sich nicht an Betrugsversuchen in Form von Social Engineering.\n\t● Wenn ein Partner nicht antwortet und es vorzieht, nicht über den Chat zu kommunizieren, respektieren Sie seine Entscheidung.\n\t● Beschränken Sie Ihre Kommunikation auf das Traden. Dieser Chat ist kein Messenger-Ersatz oder eine Trollbox.\n\t● Bleiben Sie im Gespräch freundlich und respektvoll. @@ -795,6 +793,8 @@ portfolio.pending.support.text.getHelp=Wenn Sie irgendwelche Probleme haben, kö portfolio.pending.support.button.getHelp=Trader Chat öffnen portfolio.pending.support.headline.halfPeriodOver=Zahlung überprüfen portfolio.pending.support.headline.periodOver=Die Handelsdauer ist abgelaufen +portfolio.pending.support.headline.depositTxMissing=Fehlende Einzahlungstransaktion +portfolio.pending.support.depositTxMissing=Für diesen Handel fehlt eine Einzahlungstransaktion. Öffnen Sie ein Support-Ticket, um einen Schlichter um Hilfe zu bitten. portfolio.pending.mediationRequested=Mediation beantragt portfolio.pending.refundRequested=Rückerstattung beantragt @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt und keine Handelsgebühr wurde bezahlt. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt. Ihr Angebot ist für andere Händler weiterhin verfügbar. Sie haben die Ersteller-Gebühr also nicht verloren. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.failedTrade.missingDepositTx=Eine Einzahlungstransaktion fehlt.\n\nDiese Transaktion ist erforderlich, um den Handel abzuschließen. Bitte stellen Sie sicher, dass Ihre Wallet vollständig mit der Monero-Blockchain synchronisiert ist.\n\nSie können diesen Handel in den Bereich „Fehlgeschlagene Trades“ verschieben, um ihn zu deaktivieren. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -927,8 +927,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Vermittlung support.tab.legacyArbitration.support=Legacy-Vermittlung support.tab.ArbitratorsSupportTickets={0} Tickets -support.filter=Konflikte durchsuchen -support.filter.prompt=Tragen sie Handel ID, Datum, Onion Adresse oder Kontodaten support.sigCheck.button=Signatur überprüfen support.sigCheck.popup.info=Fügen Sie die Zusammenfassungsnachricht des Schiedsverfahrens ein. Mit diesem Tool kann jeder Benutzer überprüfen, ob die Unterschrift des Schiedsrichters mit der Zusammenfassungsnachricht übereinstimmt. @@ -982,7 +980,7 @@ support.buyerTaker=XMR-Käufer/Abnehmer support.sellerTaker=XMR-Verkäufer/Abnehmer support.backgroundInfo=Haveno ist kein Unternehmen, daher behandelt es Konflikte unterschiedlich.\n\nTrader können innerhalb der Anwendung über einen sicheren Chat auf dem Bildschirm für offene Trades kommunizieren, um zu versuchen, Konflikte selbst zu lösen. Wenn das nicht ausreicht, kann ein Mediator einschreiten und helfen. Der Mediator wird die Situation bewerten und eine Auszahlung von Trade Funds vorschlagen. -support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der XMR-Käufer sind: Haben Sie die Traditional- oder Crypto-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der XMR-Verkäufer sind: Haben Sie die Traditional- oder Crypto-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Haveno verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}'' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} +support.initialInfo=Bitte geben Sie eine Beschreibung Ihres Problems in das untenstehende Textfeld ein. Fügen Sie so viele Informationen wie möglich hinzu, um die Zeit für die Konfliktlösung zu verkürzen.\n\nHier ist eine Checkliste für Informationen, die Sie angeben sollten:\n\t● Wenn Sie der XMR-Käufer sind: Haben Sie die Traditional- oder Crypto-Überweisung gemacht? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung gestartet" geklickt?\n\t● Wenn Sie der XMR-Verkäufer sind: Haben Sie die Traditional- oder Crypto-Zahlung erhalten? Wenn ja, haben Sie in der Anwendung auf die Schaltfläche "Zahlung erhalten" geklickt?\n\t● Welche Version von Haveno verwenden Sie?\n\t● Welches Betriebssystem verwenden Sie?\n\t● Wenn Sie ein Problem mit fehlgeschlagenen Transaktionen hatten, überlegen Sie bitte, in ein neues Datenverzeichnis zu wechseln.\n\t Manchmal wird das Datenverzeichnis beschädigt und führt zu seltsamen Fehlern. \n\t Siehe: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nBitte machen Sie sich mit den Grundregeln für den Konfliktprozess vertraut:\n\t● Sie müssen auf die Anfragen der {0}' innerhalb von 2 Tagen antworten.\n\t● Mediatoren antworten innerhalb von 2 Tagen. Die Vermittler antworten innerhalb von 5 Werktagen.\n\t● Die maximale Frist für einen Konflikt beträgt 14 Tage.\n\t● Sie müssen mit den {1} zusammenarbeiten und die Informationen zur Verfügung stellen, die sie anfordern, um Ihren Fall zu bearbeiten.\n\t● Mit dem ersten Start der Anwendung haben Sie die Regeln des Konfliktdokuments in der Nutzervereinbarung akzeptiert.\n\nSie können mehr über den Konfliktprozess erfahren unter: {2} support.systemMsg=Systemnachricht: {0} support.youOpenedTicket=Sie haben eine Anfrage auf Support geöffnet.\n\n{0}\n\nHaveno-Version: {1} support.youOpenedDispute=Sie haben eine Anfrage für einen Konflikt geöffnet.\n\n{0}\n\nHaveno-version: {1} @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=Darstellungsoptionen setting.preferences.showOwnOffers=Eigenen Angebote im Angebotsbuch zeigen setting.preferences.useAnimations=Animationen abspielen setting.preferences.useDarkMode=Nacht-Modus benutzen +setting.preferences.useLightMode=Leichtmodus verwenden setting.preferences.sortWithNumOffers=Marktlisten nach Anzahl der Angebote/Trades sortieren setting.preferences.onlyShowPaymentMethodsFromAccount=Nicht unterstützte Zahlungsmethoden ausblenden setting.preferences.denyApiTaker=Taker die das API nutzen vermeiden @@ -1050,6 +1049,9 @@ settings.preferences.editCustomExplorer.name=Name settings.preferences.editCustomExplorer.txUrl=Transaktions-URL settings.preferences.editCustomExplorer.addressUrl=Adress-URL +setting.info.headline=Neue Datenschutzfunktion +settings.preferences.sensitiveDataRemoval.msg=Zum Schutz der Privatsphäre von Ihnen und anderen Händlern beabsichtigt Haveno, sensible Daten aus alten Trades zu entfernen. Dies ist besonders wichtig bei Fiat-Trades, die Bankkontodaten enthalten können.\n\nEs wird empfohlen, den Wert so niedrig wie möglich zu setzen, zum Beispiel 60 Tage. Das bedeutet, dass Trades, die älter als 60 Tage sind, sensible Daten gelöscht bekommen, sofern sie abgeschlossen sind. Abgeschlossene Trades finden Sie im Portfolio- / Verlauf-Reiter. + settings.net.xmrHeader=Monero-Netzwerk settings.net.p2pHeader=Haveno-Netzwerk settings.net.onionAddressLabel=Meine Onion-Adresse @@ -1116,19 +1118,19 @@ setting.about.subsystems.label=Version des Teilsystems setting.about.subsystems.val=Netzwerkversion: {0}; P2P-Nachrichtenversion: {1}; Lokale DB-Version: {2}; Version des Handelsprotokolls: {3} setting.about.shortcuts=Shortcuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Strg + {0}'' oder ''Alt + {0}'' oder ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Strg + {0}' oder 'Alt + {0}' oder 'cmd + {0}' setting.about.shortcuts.menuNav=Hauptmenü navigieren setting.about.shortcuts.menuNav.value=Um durch das Hauptmenü zu navigieren, drücken Sie: 'Strg' oder 'Alt' oder 'cmd' mit einer numerischen Taste zwischen '1-9' setting.about.shortcuts.close=Haveno beenden -setting.about.shortcuts.close.value=''Strg + {0}'' oder ''cmd + {0}'' bzw. ''Strg + {1}'' oder ''cmd + {1}'' +setting.about.shortcuts.close.value='Strg + {0}' oder 'cmd + {0}' bzw. 'Strg + {1}' oder 'cmd + {1}' setting.about.shortcuts.closePopup=Popup- oder Dialogfenster schließen setting.about.shortcuts.closePopup.value='ESCAPE' Taste setting.about.shortcuts.chatSendMsg=Trader eine Chat-Nachricht senden -setting.about.shortcuts.chatSendMsg.value=''Strg + ENTER'' oder ''Alt + ENTER'' oder ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Strg + ENTER' oder 'Alt + ENTER' oder 'cmd + ENTER' setting.about.shortcuts.openDispute=Streitfall eröffnen setting.about.shortcuts.openDispute.value=Wählen Sie den ausstehenden Trade und klicken Sie auf: {0} @@ -1472,11 +1474,14 @@ offerDetailsWindow.countryBank=Land der Bank des Erstellers offerDetailsWindow.commitment=Verpflichtung offerDetailsWindow.agree=Ich stimme zu offerDetailsWindow.tac=Geschäftsbedingungen -offerDetailsWindow.confirm.maker=Bestätigen: Anbieten monero zu {0} -offerDetailsWindow.confirm.taker=Bestätigen: Angebot annehmen monero zu {0} +offerDetailsWindow.confirm.maker.buy=Bestätigen: Angebot zum Kauf von XMR mit {0} erstellen +offerDetailsWindow.confirm.maker.sell=Bestätigen: Angebot zum Verkauf von XMR für {0} erstellen +offerDetailsWindow.confirm.taker.buy=Bestätigen: Angebot zum Kauf von XMR mit {0} annehmen +offerDetailsWindow.confirm.taker.sell=Bestätigen: Angebot zum Verkauf von XMR für {0} annehmen offerDetailsWindow.creationDate=Erstellungsdatum offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers offerDetailsWindow.challenge=Angebots-Passphrase +offerDetailsWindow.challenge.copy=Passphrase kopieren, um sie mit Ihrem Handelspartner zu teilen qRCodeWindow.headline=QR Code qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Haveno Wallet von Ihrem externen Wallet aufzuladen. @@ -1562,7 +1567,7 @@ torNetworkSettingWindow.deleteFiles.button=Lösche veraltete Tor-Dateien und fah torNetworkSettingWindow.deleteFiles.progress=Tor wird herunterfahren torNetworkSettingWindow.deleteFiles.success=Veraltete Tor-Dateien erfolgreich gelöscht. Bitte neu starten. torNetworkSettingWindow.bridges.header=Ist Tor blockiert? -torNetworkSettingWindow.bridges.info=Falls Tor von Ihrem Provider oder in Ihrem Land blockiert wird, können Sie versuchen Tor-Bridges zu nutzen.\nBesuchen Sie die Tor-Webseite unter: https://bridges.torproject.org/bridges um mehr über Bridges und pluggable transposrts zu lernen. +torNetworkSettingWindow.bridges.info=Falls Tor von Ihrem Provider oder in Ihrem Land blockiert wird, können Sie versuchen Tor-Bridges zu nutzen.\nBesuchen Sie die Tor-Webseite unter: https://bridges.torproject.org um mehr über Bridges und pluggable transposrts zu lernen. feeOptionWindow.headline=Währung für Handelsgebührzahlung auswählen feeOptionWindow.info=Sie können wählen, die Gebühr in BSQ oder XMR zu zahlen. Wählen Sie BSQ, erhalten Sie eine vergünstigte Handelsgebühr. @@ -1622,6 +1627,7 @@ popup.warning.noPriceFeedAvailable=Es ist kein Marktpreis für diese Währung ve popup.warning.sendMsgFailed=Das Senden der Nachricht an Ihren Handelspartner ist fehlgeschlagen.\nVersuchen Sie es bitte erneut und falls es weiter fehlschlägt, erstellen Sie bitte einen Fehlerbericht. popup.warning.messageTooLong=Ihre Nachricht überschreitet die maximal erlaubte Größe. Sende Sie diese in mehreren Teilen oder laden Sie sie in einen Dienst wie https://pastebin.com hoch. popup.warning.lockedUpFunds=Sie haben gesperrtes Guthaben aus einem gescheiterten Trade.\nGesperrtes Guthaben: {0} \nEinzahlungs-Tx-Adresse: {1}\nTrade ID: {2}.\n\nBitte öffnen Sie ein Support-Ticket, indem Sie den Trade im Bildschirm "Offene Trades" auswählen und auf \"alt + o\" oder \"option + o\" drücken. +popup.warning.moneroConnection=Es gab ein Problem bei der Verbindung zum Monero-Netzwerk.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index a8f105d6c0..bec137f693 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=El valor debe ser superior a {0} createOffer.triggerPrice.invalid.tooHigh=El valor debe ser inferior a {0} # new entries -createOffer.placeOfferButton=Revisar: Poner oferta para {0} monero +createOffer.placeOfferButton.buy=Revisar: Crear oferta para comprar XMR con {0} +createOffer.placeOfferButton.sell=Revisar: Crear oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Dote de fondos su trato. # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n @@ -525,7 +521,8 @@ takeOffer.success.info=Puede ver el estado de su intercambio en \"Portafolio/Int takeOffer.error.message=Un error ocurrió al tomar la oferta.\n\n{0} # new entries -takeOffer.takeOfferButton=Revisión: Tomar oferta a {0} monero +takeOffer.takeOfferButton.buy=Revisar: Aceptar oferta para comprar XMR con {0} +takeOffer.takeOfferButton.sell=Revisar: Aceptar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=No puede tomar esta oferta porque utiliza un precio porcentual basado en el precio de mercado y no hay fuentes de precio disponibles. takeOffer.takeOfferFundWalletInfo.headline=Dotar de fondos su intercambio # suppress inspection "TrailingSpacesInProperty" @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=Desviación porcentual de precio de mercad portfolio.pending.invalidTx=Hay un problema con una transacción inválida o no encontrada.\n\nPor faovr NO envíe el pago de traditional o cryptos.\n\nAbra un ticket de soporte para obtener asistencia de un mediador.\n\nMensaje de error: {0} portfolio.pending.step1.waitForConf=Esperar a la confirmación en la cadena de bloques +portfolio.pending.step2_buyer.additionalConf=Los depósitos han alcanzado 10 confirmaciones.\nPara mayor seguridad, recomendamos esperar {0} confirmaciones antes de enviar el pago.\nProceda antes bajo su propio riesgo. portfolio.pending.step2_buyer.startPayment=Comenzar pago portfolio.pending.step2_seller.waitPaymentSent=Esperar hasta que el pago se haya iniciado portfolio.pending.step3_buyer.waitPaymentArrived=Esperar hasta que el pago haya llegado @@ -795,6 +793,8 @@ portfolio.pending.support.text.getHelp=Si tiene algún problema puede intentar c portfolio.pending.support.button.getHelp=Abrir chat de intercambio portfolio.pending.support.headline.halfPeriodOver=Comprobar pago portfolio.pending.support.headline.periodOver=El periodo de intercambio se acabó +portfolio.pending.support.headline.depositTxMissing=Transacción de depósito faltante +portfolio.pending.support.depositTxMissing=Falta una transacción de depósito para este comercio. Abra un ticket de soporte para contactar a un árbitro y recibir asistencia. portfolio.pending.mediationRequested=Mediación solicitada portfolio.pending.refundRequested=Devolución de fondos solicitada @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Ya ha aceptado portfolio.pending.failedTrade.taker.missingTakerFeeTx=Falta la transacción de tasa de tomador\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos y no se ha pagado ninguna tasa de intercambio. Puede mover esta operación a intercambios fallidos. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Falta la transacción de tasa de tomador de su par.\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos. Su oferta aún está disponible para otros comerciantes, por lo que no ha perdido la tasa de tomador. Puede mover este intercambio a intercambios fallidos. -portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. +portfolio.pending.failedTrade.missingDepositTx=Falta una transacción de depósito.\n\nEsta transacción es necesaria para completar la operación. Por favor, asegúrate de que tu monedero esté completamente sincronizado con la cadena de bloques de Monero.\n\nPuedes mover esta operación a la sección de "Operaciones Fallidas" para desactivarla. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.errorMsgSet=Hubo un error durante la ejecución del protocolo de intercambio.\n\nError: {0}\n\nPuede ser que este error no sea crítico y que el intercambio se pueda completar normalmente. Si no está seguro, abra un ticket de mediación para obtener consejos de los mediadores de Haveno.\n\nSi el error fue crítico y la operación no se puede completar, es posible que haya perdido su tarifa de operación. Solicite un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:ttps://github.com/bisq-network/support/issues]. @@ -928,8 +928,6 @@ support.tab.mediation.support=Mediación support.tab.arbitration.support=Arbitraje support.tab.legacyArbitration.support=Legado de arbitraje support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Buscar disputas -support.filter.prompt=Introduzca ID de transacción, fecha, dirección onion o datos de cuenta. support.sigCheck.button=Comprobar firma support.sigCheck.popup.info=Pegue el mensaje resumido del proceso de arbitraje. Con esta herramienta, cualquier usuario puede verificar si la firma del árbitro coincide con el mensaje resumido. @@ -1036,6 +1034,7 @@ setting.preferences.displayOptions=Mostrar opciones setting.preferences.showOwnOffers=Mostrar mis propias ofertas en el libro de ofertas setting.preferences.useAnimations=Usar animaciones setting.preferences.useDarkMode=Usar modo oscuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por número de ofertas/intercambios setting.preferences.onlyShowPaymentMethodsFromAccount=Ocultar métodos de pago no soportados setting.preferences.denyApiTaker=Denegar tomadores usando la misma API @@ -1051,6 +1050,9 @@ settings.preferences.editCustomExplorer.name=Nombre settings.preferences.editCustomExplorer.txUrl=URL de transacción settings.preferences.editCustomExplorer.addressUrl=URL de la dirección +setting.info.headline=Nueva función de privacidad de datos +settings.preferences.sensitiveDataRemoval.msg=Para proteger la privacidad de usted y otros traders, Haveno pretende eliminar datos sensibles de las operaciones antiguas. Esto es especialmente importante para las operaciones con fiat que pueden incluir detalles de cuentas bancarias.\n\nSe recomienda configurarlo lo más bajo posible, por ejemplo, 60 días. Esto significa que las operaciones de hace más de 60 días tendrán los datos sensibles eliminados, siempre que estén completadas. Las operaciones completadas se encuentran en la pestaña Portafolio / Historial. + settings.net.xmrHeader=Red Monero settings.net.p2pHeader=Red Haveno settings.net.onionAddressLabel=Mi dirección onion @@ -1117,19 +1119,19 @@ setting.about.subsystems.label=Versión de subsistemas: setting.about.subsystems.val=Versión de red: {0}; Versión de mensajes P2P: {1}; Versión de Base de Datos local: {2}; Versión de protocolo de intercambio {3} setting.about.shortcuts=Atajos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar menú principal setting.about.shortcuts.menuNav.value=Para navegar por el menú principal pulse: 'Ctrl' or 'alt' or 'cmd' con una tecla numérica entre el '1-9' setting.about.shortcuts.close=Cerrar Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Cerrar la ventana emergente o ventana de diálogo setting.about.shortcuts.closePopup.value=Tecla 'ESCAPE' setting.about.shortcuts.chatSendMsg=Enviar mensaje en chat de intercambio -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Seleccionar intercambios pendientes y pulsar: {0} @@ -1473,11 +1475,14 @@ offerDetailsWindow.countryBank=País del banco del creador offerDetailsWindow.commitment=Compromiso offerDetailsWindow.agree=Estoy de acuerdo offerDetailsWindow.tac=Términos y condiciones: -offerDetailsWindow.confirm.maker=Confirmar: Poner oferta para {0} monero -offerDetailsWindow.confirm.taker=Confirmar: Tomar oferta {0} monero +offerDetailsWindow.confirm.maker.buy=Confirmar: Crear oferta para comprar XMR con {0} +offerDetailsWindow.confirm.maker.sell=Confirmar: Crear oferta para vender XMR por {0} +offerDetailsWindow.confirm.taker.buy=Confirmar: Aceptar oferta para comprar XMR con {0} +offerDetailsWindow.confirm.taker.sell=Confirmar: Aceptar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Fecha de creación offerDetailsWindow.makersOnion=Dirección onion del creador offerDetailsWindow.challenge=Frase de contraseña de la oferta +offerDetailsWindow.challenge.copy=Copiar frase de contraseña para compartir con tu contraparte qRCodeWindow.headline=Código QR qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Haveno desde su billetera externa. @@ -1563,7 +1568,7 @@ torNetworkSettingWindow.deleteFiles.button=Borrar archivos Tor desactualizados y torNetworkSettingWindow.deleteFiles.progress=Cerrado de Tor en proceso torNetworkSettingWindow.deleteFiles.success=Archivos Tor desactualizados borrados. Por favor, reinice. torNetworkSettingWindow.bridges.header=¿Está Tor bloqueado? -torNetworkSettingWindow.bridges.info=Si Tor está bloqueado por su proveedor de internet o por su país puede intentar usar puentes Tor.\nVisite la página web Tor en https://bridges.torproject.org/bridges para saber más acerca de los puentes y transportes conectables. +torNetworkSettingWindow.bridges.info=Si Tor está bloqueado por su proveedor de internet o por su país puede intentar usar puentes Tor.\nVisite la página web Tor en https://bridges.torproject.org para saber más acerca de los puentes y transportes conectables. feeOptionWindow.headline=Elija la moneda para el pago de la comisiones de intercambio feeOptionWindow.info=Puede elegir pagar la tasa de intercambio en BSQ o XMR. Si elige BSQ apreciará la comisión de intercambio descontada. @@ -1623,6 +1628,7 @@ popup.warning.noPriceFeedAvailable=No hay una fuente de precios disponible para popup.warning.sendMsgFailed=El envío de mensaje a su compañero de intercambio falló.\nPor favor, pruebe de nuevo y si continúa fallando, reporte el fallo. popup.warning.messageTooLong=Su mensaje excede el tamaño máximo permitido. Por favor, envíelo por partes o súbalo a un servicio como https://pastebin.com popup.warning.lockedUpFunds=Ha bloqueado fondos de un intercambio fallido.\nBalance bloqueado: {0}\nDirección de depósito TX: {1}\nID de intercambio: {2}.\n\nPor favor, abra un ticket de soporte seleccionando el intercambio en la pantalla de intercambios pendientes y haciendo clic en \"alt + o\" o \"option + o\"." +popup.warning.moneroConnection=Hubo un problema al conectar con la red de Monero.\n\n{0} popup.warning.makerTxInvalid=Esta oferta no es válida. Por favor seleccione otra oferta diferente.\n\n takeOffer.cancelButton=Cancelar toma de oferta diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index dd0cbee6d3..725353f936 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent= ٪ زیر قیمت بازار shared.aboveInPercent= ٪ بالای قیمت بازار shared.enterPercentageValue=ارزش ٪ را وارد کنید shared.OR=یا -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=در انتظار دریافت وجه... shared.TheXMRBuyer=خریدار بیتکوین shared.You=شما @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=فیلتر shared.enabled=Enabled @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=با XMR خرید کنید: offerbook.sellXmrFor=فروش XMR برای: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} روز offerbook.timeSinceSigning.notSigned.noNeed=بدون پاسخ @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=این روش پرداخت موقتاً تا {1} به {0} محدود شده است زیرا همه خریداران حساب‌های جدیدی دارند.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=پیشنهاد شما تنها مختص خریدارانی خواهد بود که حساب‌هایی با امضا و سنین پیر دارند زیرا این مبلغ {0} را بیشتر می‌کند.\n\n{1} @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=بررسی: پیشنهاد را برای {0} بیتکوین بگذارید +createOffer.placeOfferButton.buy=بررسی: ایجاد پیشنهاد خرید XMR با {0} +createOffer.placeOfferButton.sell=بررسی: ایجاد پیشنهاد فروش XMR به ازای {0} createOffer.createOfferFundWalletInfo.headline=پیشنهاد خود را تامین وجه نمایید # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=مقدار معامله:{0}\n @@ -524,7 +520,8 @@ takeOffer.success.info=شما می‌توانید وضعیت معامله‌ی takeOffer.error.message=هنگام قبول کردن پیشنهاد، اتفاقی رخ داده است.\n\n{0} # new entries -takeOffer.takeOfferButton=بررسی: برای {0} بیتکوین پیشنهاد بگذارید. +takeOffer.takeOfferButton.buy=بررسی: قبول پیشنهاد خرید XMR با {0} +takeOffer.takeOfferButton.sell=بررسی: قبول پیشنهاد فروش XMR به ازای {0} takeOffer.noPriceFeedAvailable=امکان پذیرفتن پیشنهاد وجود ندارد. پیشنهاد از قیمت درصدی مبتنی بر قیمت روز بازار استفاده می‌کند و قیمت‌های بازار هم‌اکنون در دسترس نیست. takeOffer.takeOfferFundWalletInfo.headline=معامله خود را تأمین وجه نمایید # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=برای تأییدیه بلاک چین منتظر باشید +portfolio.pending.step2_buyer.additionalConf=واریزها به ۱۰ تأیید رسیده‌اند.\nبرای امنیت بیشتر، توصیه می‌کنیم قبل از ارسال پرداخت، {0} تأیید صبر کنید.\nاقدام زودهنگام با مسئولیت خودتان است. portfolio.pending.step2_buyer.startPayment=آغاز پرداخت portfolio.pending.step2_seller.waitPaymentSent=صبر کنید تا پرداخت شروع شود portfolio.pending.step3_buyer.waitPaymentArrived=صبر کنید تا پرداخت حاصل شود @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Mo # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=لطفا با استفاده از راه‌های ارتباطی ارائه شده توسط فروشنده با وی تماس بگیرید و قرار ملاقاتی را برای پرداخت {0} تنظیم کنید.\n portfolio.pending.step2_buyer.startPaymentUsing=آغاز پرداخت با استفاده از {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=اطلاعات تماس خرید portfolio.pending.step2_seller.waitPayment.msg=تراکنش سپرده، حداقل یک تأییدیه بلاکچین دارد.شما\nباید تا آغاز پرداخت {0} از جانب خریدار بیتکوین، صبر نمایید. portfolio.pending.step2_seller.warn=خریدار بیت‌کوین هنوز پرداخت {0} را انجام نداده است.\nشما باید تا آغاز پرداخت از جانب او، صبر نمایید.\nاگر معامله تا {1} تکمیل نشد، داور بررسی خواهد کرد. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +713,7 @@ portfolio.pending.step3_seller.westernUnion=خریدار باید MTCN (شمار portfolio.pending.step3_seller.halCash=خریدار باید کد HalCash را برای شما با پیامک بفرستد. علاوه‌ برآن شما از HalCash پیامی را محتوی اطلاعات موردنیاز برای برداشت EUR از خودپردازهای پشتیبان HalCash دریافت خواهید کرد.\n\nپس از اینکه پول را از دستگاه خودپرداز دریافت کردید، لطفا در اینجا رسید پرداخت را تایید کنید. portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=تأیید رسید پرداخت @@ -737,7 +735,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=آیا وجه {0} را از شریک معاملاتی خود دریافت کرده‌اید؟\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=تأیید کنید که وجه را دریافت کرده‌اید @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over +portfolio.pending.support.headline.depositTxMissing=تراکنش واریز مفقود شده +portfolio.pending.support.depositTxMissing=برای این معامله، تراکنش واریز وجود ندارد. برای دریافت کمک با داور، یک تیکت پشتیبانی باز کنید. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested @@ -811,15 +811,15 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=یک تراکنش واریز مفقود است.\n\nاین تراکنش برای تکمیل معامله لازم است. لطفاً اطمینان حاصل کنید که کیف پول شما به‌طور کامل با بلاک‌چین مونرو همگام‌سازی شده است.\n\nمی‌توانید این معامله را به بخش «معاملات ناموفق» منتقل کنید تا غیرفعال شود. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -979,7 +977,7 @@ support.sellerMaker=فروشنده/سفارش گذار بیتکوین support.buyerTaker=خریدار/پذیرنده‌ی بیتکوین support.sellerTaker=فروشنده/پذیرنده‌ی بیتکوین -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=پیغام سیستم: {0} support.youOpenedTicket=شما یک درخواست برای پشتیبانی باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} support.youOpenedDispute=شما یک درخواست برای یک اختلاف باز کردید.\n\n{0}\n\nنسخه Haveno شما: {1} @@ -987,8 +985,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=افزودن آلتکوین setting.preferences.displayOptions=نمایش گزینه‌ها setting.preferences.showOwnOffers=نمایش پیشنهادهای من در دفتر پیشنهاد setting.preferences.useAnimations=استفاده از انیمیشن‌ها -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=حالت تاریک را استفاده کنید +setting.preferences.useLightMode=حالت روشن را استفاده کنید setting.preferences.sortWithNumOffers=مرتب سازی لیست‌ها با تعداد معاملات/پیشنهادها setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1047,6 +1046,9 @@ settings.preferences.editCustomExplorer.name=نام settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=ویژگی جدید حفظ حریم خصوصی داده‌ها +settings.preferences.sensitiveDataRemoval.msg=برای محافظت از حریم خصوصی شما و سایر معامله‌گران، Haveno قصد دارد داده‌های حساس مربوط به معاملات قدیمی را حذف کند. این موضوع به ویژه برای معاملات فیات که ممکن است شامل جزئیات حساب بانکی باشند اهمیت دارد.\n\nتوصیه می‌شود این مقدار را تا حد امکان پایین تنظیم کنید، مثلاً ۶۰ روز. این بدان معناست که معاملاتی که بیش از ۶۰ روز از آنها گذشته و تکمیل شده‌اند، داده‌های حساس آنها پاک خواهد شد. معاملات تکمیل شده در تب «پرتفوی / تاریخچه» یافت می‌شوند. + settings.net.xmrHeader=شبکه بیتکوین settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=آدرس onion من @@ -1113,19 +1115,19 @@ setting.about.subsystems.label=نسخه‌های زیرسیستم‌ها setting.about.subsystems.val=نسخه ی شبکه: {0}; نسخه ی پیام همتا به همتا: {1}; نسخه ی Local DB: {2}; نسخه پروتکل معامله: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1200,7 +1202,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=حساب‌های آلت‌کوین شما -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=من می فهمم و تأیید می کنم که می دانم از کدام کیف پول باید استفاده کنم. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1315,7 +1317,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=نوع پیشن account.notifications.marketAlert.message.title=هشدار پیشنهاد account.notifications.marketAlert.message.msg.below=پایین account.notifications.marketAlert.message.msg.above=بالای -account.notifications.marketAlert.message.msg=پیشنهاد جدید ''{0} {1}'' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت ''{5}'' در دفتر پیشنهادات Haveno منتشر شده است.\nشناسه پیشنهاد: {6}. +account.notifications.marketAlert.message.msg=پیشنهاد جدید '{0} {1}' با قیمت {2} ({3} {4} قیمت بازار) و روش پرداخت '{5}' در دفتر پیشنهادات Haveno منتشر شده است.\nشناسه پیشنهاد: {6}. account.notifications.priceAlert.message.title=هشدار قیمت برای {0} account.notifications.priceAlert.message.msg=هشدار قیمت شما فعال شده است. قیمت {0} فعلی {1} {2} است account.notifications.noWebCamFound.warning=دوبین پیدا نشد.\n\nلطفا از گزینه ایمیل برای ارسال توکن و کلید رمزنگاری از تلفن همراهتان به برنامه Haveno استفاده کنید. @@ -1468,11 +1470,14 @@ offerDetailsWindow.countryBank=کشور بانک سفارش‌گذار offerDetailsWindow.commitment=تعهد offerDetailsWindow.agree=من موافقم offerDetailsWindow.tac=شرایط و الزامات -offerDetailsWindow.confirm.maker=تأیید: پیشنهاد را به {0} بگذارید -offerDetailsWindow.confirm.taker=تأیید: پیشنهاد را به {0} بپذیرید +offerDetailsWindow.confirm.maker.buy=تأیید: ایجاد پیشنهاد خرید XMR با {0} +offerDetailsWindow.confirm.maker.sell=تأیید: ایجاد پیشنهاد فروش XMR به ازای {0} +offerDetailsWindow.confirm.taker.buy=تأیید: قبول پیشنهاد خرید XMR با {0} +offerDetailsWindow.confirm.taker.sell=تأیید: قبول پیشنهاد فروش XMR به ازای {0} offerDetailsWindow.creationDate=تاریخ ایجاد offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار offerDetailsWindow.challenge=Passphrase de l'offre +offerDetailsWindow.challenge.copy=عبارت عبور را برای به اشتراک‌گذاری با همتا کپی کنید qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1558,7 +1563,7 @@ torNetworkSettingWindow.deleteFiles.button=حذف فایل های قدیمی Tor torNetworkSettingWindow.deleteFiles.progress=خاموش کردن Tor در حال انجام است torNetworkSettingWindow.deleteFiles.success=حذف فایل های قدیمی Tor با موفقیت انجام شد. لطفاً مجدداً راه اندازی کنید. torNetworkSettingWindow.bridges.header=آیا Tor مسدود شده است؟ -torNetworkSettingWindow.bridges.info=اگر Tor توسط ارائه دهنده اینترنت شما یا توسط کشور شما مسدود شده است، شما می توانید از پل های Tor استفاده کنید.\nاز صفحه وب Tor در https://bridges.torproject.org/bridges دیدن کنید تا مطالب بیشتری در مورد پل ها و نقل و انتقالات قابل اتصال یاد بگیرید. +torNetworkSettingWindow.bridges.info=اگر Tor توسط ارائه دهنده اینترنت شما یا توسط کشور شما مسدود شده است، شما می توانید از پل های Tor استفاده کنید.\nاز صفحه وب Tor در https://bridges.torproject.org دیدن کنید تا مطالب بیشتری در مورد پل ها و نقل و انتقالات قابل اتصال یاد بگیرید. feeOptionWindow.headline=انتخاب ارز برای پرداخت هزینه معامله feeOptionWindow.info=شما می توانید انتخاب کنید که هزینه معامله را در BSQ یا در XMR بپردازید. اگر BSQ را انتخاب می کنید، از تخفیف هزینه معامله برخوردار می شوید. @@ -1618,6 +1623,7 @@ popup.warning.noPriceFeedAvailable=برای این ارز هیچ خوراک قی popup.warning.sendMsgFailed=ارسال پیام به شریک معاملاتی شما ناموفق بود. \nلطفا دوباره امتحان کنید و اگر همچنان ناموفق بود، گزارش یک اشکال را ارسال کنید. popup.warning.messageTooLong=پیام شما بیش از حداکثر اندازه مجاز است. لطفا آن را در چند بخش ارسال کنید یا آن را در یک سرویس مانند https://pastebin.com آپلود کنید. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." +popup.warning.moneroConnection=مشکلی در اتصال به شبکه مونرو رخ داده است.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1630,7 +1636,7 @@ popup.warning.priceRelay=رله قیمت popup.warning.seed=دانه popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=ما شیء فیلتر را از گره‌های اولیه دریافت نکردیم. لطفاً به مدیران شبکه اطلاع دهید که یک شیء فیلتر ثبت کنند. -popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. +popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. @@ -1679,8 +1685,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1975,8 +1981,8 @@ payment.accountType=نوع حساب payment.checking=بررسی payment.savings=اندوخته ها payment.personalId=شناسه شخصی -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=زمانی که از HalCash استفاده می‌کنید، خریدار باید کد HalCash را از طریق پیام کوتاه موبایل به فروشنده XMR ارسال کند.\n\nلطفا مطمئن شوید که از حداکثر میزانی که بانک شما برای انتقال از طریق HalCash مجاز می‌داند تجاوز نکرده‌اید. حداقل مقداردر هر برداشت معادل 10 یورو و حداکثر مقدار 600 یورو می‌باشد. این محدودیت برای برداشت‌های تکراری برای هر گیرنده در روز 3000 یورو و در ماه 6000 یورو می‌باشد. لطفا این محدودیت‌ها را با بانک خود مطابقت دهید و مطمئن شوید که آنها هم همین محدودی‌ها را دارند.\n\nمقدار برداشت باید شریبی از 10 یورو باشد چرا که مقادیر غیر از این را نمی‌توانید از طریق ATM برداشت کنید. رابط کاربری در صفحه ساخت پینشهاد و پذیرش پیشنهاد مقدار XMR را به گونه‌ای تنظیم می‌کنند که مقدار EUR درست باشد. شما نمی‌توانید از قیمت بر مبنای بازار استفاده کنید چون مقدار یورو با تغییر قیمت‌ها عوض خواهد شد.\n\nدر صورت بروز اختلاف خریدار XMR باید شواهد مربوط به ارسال یورو را ارائه دهد. @@ -1988,7 +1994,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=لطفاً توجه داشته باشید که Cash App ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. @@ -2022,7 +2028,7 @@ payment.japan.recipient=نام payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=برای حفاظت از شما، به شدت از استفاده از پین‌های Paysafecard برای پرداخت جلوگیری می‌کنیم.\n\n\ تراکنش‌های انجام شده از طریق پین‌ها نمی‌توانند به طور مستقل برای حل اختلاف تأیید شوند. اگر مشکلی پیش آید، بازیابی وجوه ممکن است غیرممکن باشد.\n\n\ برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index f4395ca9c4..fff87395f5 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -146,7 +141,7 @@ shared.createNewAccountDescription=Les détails de votre compte sont stockés lo shared.saveNewAccount=Sauvegarder un nouveau compte shared.selectedAccount=Sélectionner un compte shared.deleteAccount=Supprimer le compte -shared.errorMessageInline=\nMessage d''erreur: {0} +shared.errorMessageInline=\nMessage d'erreur: {0} shared.errorMessage=Message d'erreur shared.information=Information shared.name=Nom @@ -171,14 +166,14 @@ shared.enterPrivKey=Entrer la clé privée pour déverrouiller shared.payoutTxId=ID du versement de la transaction shared.contractAsJson=Contrat au format JSON shared.viewContractAsJson=Voir le contrat en format JSON -shared.contract.title=Contrat pour la transaction avec l''ID : {0} +shared.contract.title=Contrat pour la transaction avec l'ID : {0} shared.paymentDetails=XMR {0} détails du paiement shared.securityDeposit=Dépôt de garantie shared.yourSecurityDeposit=Votre dépôt de garantie shared.contract=Contrat shared.messageArrived=Message reçu. shared.messageStoredInMailbox=Message stocké dans la boîte de réception. -shared.messageSendingFailed=Échec de l''envoi du message. Erreur: {0} +shared.messageSendingFailed=Échec de l'envoi du message. Erreur: {0} shared.unlock=Déverrouiller shared.toReceive=à recevoir shared.toSpend=à dépenser @@ -277,7 +272,7 @@ mainView.p2pNetworkWarnMsg.noNodesAvailable=Il n'y a pas de noeud de seed ou de mainView.p2pNetworkWarnMsg.connectionToP2PFailed=La connexion au réseau Haveno a échoué (erreur signalé: {0}).\nVeuillez vérifier votre connexion internet ou essayez de redémarrer l'application. mainView.walletServiceErrorMsg.timeout=La connexion au réseau Monero a échoué car le délai d'attente a expiré. -mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Monero a échoué à cause d''une erreur: {0} +mainView.walletServiceErrorMsg.connectionError=La connexion au réseau Monero a échoué à cause d'une erreur: {0} mainView.walletServiceErrorMsg.rejectedTxException=Le réseau a rejeté une transaction.\n\n{0} @@ -331,8 +326,8 @@ market.trades.showVolumeInUSD=Afficher le volume en USD offerbook.createOffer=Créer un ordre offerbook.takeOffer=Accepter un ordre -offerbook.takeOfferToBuy=Accepter l''ordre d''achat {0} -offerbook.takeOfferToSell=Accepter l''ordre de vente {0} +offerbook.takeOfferToBuy=Accepter l'ordre d'achat {0} +offerbook.takeOfferToSell=Accepter l'ordre de vente {0} offerbook.takeOffer.enterChallenge=Entrez la phrase secrète de l'offre offerbook.trader=Échanger offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Est-ce-que la confirmation automatique est activée offerbook.buyXmrWith=Acheter XMR avec : offerbook.sellXmrFor=Vendre XMR pour : -offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d''un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d''un autre pair. +offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d'un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d'un autre pair. offerbook.timeSinceSigning.notSigned=Pas encore signé offerbook.timeSinceSigning.notSigned.ageDays={0} jours offerbook.timeSinceSigning.notSigned.noNeed=N/A @@ -368,27 +363,27 @@ shared.notSigned.noNeed=Ce type de compte ne nécessite pas de signature shared.notSigned.noNeedDays=Ce type de compte ne nécessite pas de signature et a été créée il y'a {0} jours shared.notSigned.noNeedAlts=Les comptes pour crypto ne supportent pas la signature ou le vieillissement -offerbook.nrOffers=Nombre d''ordres: {0} +offerbook.nrOffers=Nombre d'ordres: {0} offerbook.volume={0} (min - max) offerbook.deposit=Déposer XMR (%) offerbook.deposit.help=Les deux parties à la transaction ont payé un dépôt pour assurer que la transaction se déroule normalement. Ce montant sera remboursé une fois la transaction terminée. offerbook.createNewOffer=Créer une offre à {0} {1} -offerbook.createOfferToBuy=Créer un nouvel ordre d''achat pour {0} +offerbook.createOfferToBuy=Créer un nouvel ordre d'achat pour {0} offerbook.createOfferToSell=Créer un nouvel ordre de vente pour {0} -offerbook.createOfferToBuy.withTraditional=Créer un nouvel ordre d''achat pour {0} avec {1} +offerbook.createOfferToBuy.withTraditional=Créer un nouvel ordre d'achat pour {0} avec {1} offerbook.createOfferToSell.forTraditional=Créer un nouvel ordre de vente pour {0} for {1} offerbook.createOfferToBuy.withCrypto=Créer un nouvel ordre de vente pour {0} (achat{1}) -offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d''achat pour {0} (vente{1}) +offerbook.createOfferToSell.forCrypto=Créer un nouvel ordre d'achat pour {0} (vente{1}) offerbook.takeOfferButton.tooltip=Accepter un ordre pour {0} offerbook.yesCreateOffer=Oui, créer un ordre offerbook.setupNewAccount=Configurer un nouveau compte de change offerbook.removeOffer.success=L'ordre a bien été retiré. -offerbook.removeOffer.failed=Le retrait de l''ordre a échoué:\n{0} -offerbook.deactivateOffer.failed=La désactivation de l''ordre a échoué:\n{0} -offerbook.activateOffer.failed=La publication de l''ordre a échoué:\n{0} -offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l''écran {0}. +offerbook.removeOffer.failed=Le retrait de l'ordre a échoué:\n{0} +offerbook.deactivateOffer.failed=La désactivation de l'ordre a échoué:\n{0} +offerbook.activateOffer.failed=La publication de l'ordre a échoué:\n{0} +offerbook.withdrawFundsHint=Vous pouvez retirer les fonds investis depuis l'écran {0}. offerbook.warning.noTradingAccountForCurrency.headline=Aucun compte de paiement pour la devise sélectionnée offerbook.warning.noTradingAccountForCurrency.msg=Vous n'avez pas de compte de paiement mis en place pour la devise sélectionnée.\n\nVoudriez-vous créer une offre pour une autre devise à la place? @@ -399,8 +394,8 @@ offerbook.warning.counterpartyTradeRestrictions=Cette offre ne peut être accept offerbook.warning.newVersionAnnouncement=Grâce à cette version du logiciel, les partenaires commerciaux peuvent confirmer et vérifier les comptes de paiement de chacun pour créer un réseau de comptes de paiement de confiance.\n\nUne fois la transaction réussie, votre compte de paiement sera vérifié et les restrictions de transaction seront levées après une certaine période de temps (cette durée est basée sur la méthode de vérification).\n\nPour plus d'informations sur la vérification de votre compte, veuillez consulter le document sur https://docs.haveno.exchange/payment-methods#account-signing -popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l''acheteur n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l''acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} -popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n''a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Le compte de l'acheteur n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature du compte de l'acheteur est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Le montant de transaction autorisé est limité à {0} en raison des restrictions de sécurité basées sur les critères suivants:\n- Votre compte n'a pas été signé par un arbitre ou par un pair\n- Le délai depuis la signature de votre compte est inférieur à 30 jours\n- Le mode de paiement pour cette offre est considéré comme présentant un risque de rétrofacturation bancaire\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Ce mode de paiement est temporairement limité à {0} jusqu'à {1} car tous les acheteurs ont de nouveaux comptes.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Votre offre sera limitée aux acheteurs avec des comptes signés et anciens car elle dépasse {0}.\n\n{1} @@ -421,7 +416,7 @@ offerbook.info.sellAboveMarketPrice=Vous obtiendrez {0} de plus que le prix actu offerbook.info.buyBelowMarketPrice=Vous paierez {0} de moins que le prix actuel du marché (mis à jour chaque minute). offerbook.info.buyAtFixedPrice=Vous achèterez à ce prix déterminé. offerbook.info.sellAtFixedPrice=Vous vendrez à ce prix déterminé. -offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l''arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. +offerbook.info.noArbitrationInUserLanguage=En cas de litige, veuillez noter que l'arbitrage de cet ordre sera traité par {0}. La langue est actuellement définie sur {1}. offerbook.info.roundedFiatVolume=Le montant a été arrondi pour accroître la confidentialité de votre transaction. #################################################################### @@ -440,7 +435,7 @@ createOffer.fundsBox.title=Financer votre ordre createOffer.fundsBox.offerFee=Frais de transaction createOffer.fundsBox.networkFee=Frais de minage createOffer.fundsBox.placeOfferSpinnerInfo=Publication de l'ordre en cours ... -createOffer.fundsBox.paymentLabel=Transaction Haveno avec l''ID {0} +createOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} createOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) createOffer.success.headline=Votre offre a été créée createOffer.success.info=Vous pouvez gérer vos ordres en cours dans \"Portfolio/Mes ordres\". @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=La valeur doit être supérieure à {0} createOffer.triggerPrice.invalid.tooHigh=La valuer doit être inférieure à {0] # new entries -createOffer.placeOfferButton=Review: Placer un ordre de {0} monero +createOffer.placeOfferButton.buy=Vérifier : Créer une offre pour acheter XMR avec {0} +createOffer.placeOfferButton.sell=Vérifier : Créer une offre pour vendre XMR contre {0} createOffer.createOfferFundWalletInfo.headline=Financer votre ordre # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=Montant du trade: {0}\n\n @@ -472,7 +468,7 @@ createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} à cette offre - Frais de transaction : {3} # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) -createOffer.amountPriceBox.error.message=Une erreur s''est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n''a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l''application et vérifier votre connexion réseau. +createOffer.amountPriceBox.error.message=Une erreur s'est produite lors du placement de cet ordre:\n\n{0}\n\nAucun fonds n'a été prélevé sur votre portefeuille pour le moment.\nVeuillez redémarrer l'application et vérifier votre connexion réseau. createOffer.setAmountPrice=Définir le montant et le prix createOffer.warnCancelOffer=Vous avez déjà financé cet ordre.\nSi vous annulez maintenant, vos fonds seront envoyés dans votre portefeuille haveno local et seront disponible pour retrait dans l'onglet \"Fonds/Envoyer des fonds\".\nÊtes-vous certain de vouloir annuler ? createOffer.timeoutAtPublishing=Un timeout est survenu au moment de la publication de l'ordre. @@ -482,7 +478,7 @@ createOffer.tooLowSecDeposit.makerIsSeller=Ceci vous donne moins de protection d createOffer.tooLowSecDeposit.makerIsBuyer=cela offre moins de protection pour le pair que de suivre le protocole de trading car vous avez moins de dépôt à risque. D'autres utilisateurs préféreront peut-être accepter d'autres ordres que le vôtre. createOffer.resetToDefault=Non, revenir à la valeur par défaut createOffer.useLowerValue=Oui, utiliser ma valeur la plus basse -createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l''écart max. du prix du marché autorisé\nL''écart maximum autorisé est {0} et peut être ajusté dans les préférences. +createOffer.priceOutSideOfDeviation=Le prix que vous avez fixé est en dehors de l'écart max. du prix du marché autorisé\nL'écart maximum autorisé est {0} et peut être ajusté dans les préférences. createOffer.changePrice=Modifier le prix createOffer.tac=En plaçant cet ordre vous acceptez d'effectuer des transactions avec n'importe quel trader remplissant les conditions affichées à l'écran. createOffer.currencyForFee=Frais de transaction @@ -490,7 +486,7 @@ createOffer.setDeposit=Etablir le dépôt de garantie de l'acheteur (%) createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'acheteur (%) createOffer.setDepositForBothTraders=Établissez le dépôt de sécurité des deux traders (%) createOffer.securityDepositInfo=Le dépôt de garantie de votre acheteur sera de {0} -createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu''acheteur sera de {0} +createOffer.securityDepositInfoAsBuyer=Votre dépôt de garantie en tant qu'acheteur sera de {0} createOffer.minSecurityDepositUsed=Le dépôt de sécurité minimum est utilisé createOffer.buyerAsTakerWithoutDeposit=Aucun dépôt requis de la part de l'acheteur (protégé par un mot de passe) createOffer.myDeposit=Mon dépôt de garantie (%) @@ -516,16 +512,17 @@ takeOffer.fundsBox.tradeAmount=Montant à vendre takeOffer.fundsBox.offerFee=Frais de transaction du trade takeOffer.fundsBox.networkFee=Total des frais de minage takeOffer.fundsBox.takeOfferSpinnerInfo=Acceptation de l'offre : {0} -takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l''ID {0} +takeOffer.fundsBox.paymentLabel=Transaction Haveno avec l'ID {0} takeOffer.fundsBox.fundsStructure=({0} dépôt de garantie, {1} frais de transaction, {2} frais de minage) takeOffer.fundsBox.noFundingRequiredTitle=Aucun financement requis takeOffer.fundsBox.noFundingRequiredDescription=Obtenez la phrase secrète de l'offre auprès du vendeur en dehors de Haveno pour accepter cette offre. takeOffer.success.headline=Vous avez accepté un ordre avec succès. takeOffer.success.info=Vous pouvez voir vos transactions dans \"Portfolio/Échanges en cours\". -takeOffer.error.message=Une erreur s''est produite pendant l’'acceptation de l''ordre.\n\n{0} +takeOffer.error.message=Une erreur s'est produite pendant l’'acceptation de l'ordre.\n\n{0} # new entries -takeOffer.takeOfferButton=Vérifier: Accepter l''ordre de {0} Monero +takeOffer.takeOfferButton.buy=Vérifier : Accepter une offre pour acheter XMR avec {0} +takeOffer.takeOfferButton.sell=Vérifier : Accepter une offre pour vendre XMR contre {0} takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui-ci utilise un prix en pourcentage basé sur le prix du marché, mais il n'y a pas de prix de référence de disponible. takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade # suppress inspection "TrailingSpacesInProperty" @@ -561,7 +558,7 @@ openOffer.triggered=Cette offre a été désactivée car le prix du marché a at editOffer.setPrice=Définir le prix editOffer.confirmEdit=Confirmation: Modification de l'ordre editOffer.publishOffer=Publication de votre ordre. -editOffer.failed=Échec de la modification de l''ordre:\n{0} +editOffer.failed=Échec de la modification de l'ordre:\n{0} editOffer.success=Votre ordre a été modifié avec succès. editOffer.invalidDeposit=Le dépôt de garantie de l'acheteur ne respecte pas le cadre des contraintes définies par Haveno DAO et ne peut plus être modifié. @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=Pourcentage de déviation du prix par rapp portfolio.pending.invalidTx=Il y'a un problème avec une transaction manquante ou invalide.\n\nVeuillez NE PAS envoyer le payement Traditional ou crypto.\n\nOuvrez un ticket de support pour avoir l'aide d'un médiateur.\n\nMessage d'erreur: {0} portfolio.pending.step1.waitForConf=Attendre la confirmation de la blockchain +portfolio.pending.step2_buyer.additionalConf=Les dépôts ont atteint 10 confirmations.\nPour plus de sécurité, nous recommandons d’attendre {0} confirmations avant d’envoyer le paiement.\nProcédez plus tôt à vos propres risques. portfolio.pending.step2_buyer.startPayment=Initier le paiement portfolio.pending.step2_seller.waitPaymentSent=Patientez jusqu'à ce que le paiement soit commencé. portfolio.pending.step3_buyer.waitPaymentArrived=Patientez jusqu'à la réception du paiement @@ -633,20 +631,20 @@ portfolio.pending.step2_buyer.crypto=Veuillez transférer à partir de votre por portfolio.pending.step2_buyer.cash=Veuillez vous rendre dans une banque et payer {0} au vendeur de XMR.\n portfolio.pending.step2_buyer.cash.extra=CONDITIONS REQUISES: \nAprès avoir effectué le paiement veuillez écrire sur le reçu papier : PAS DE REMBOURSEMENT.\nPuis déchirer le en 2, prenez en une photo et envoyer le à l'adresse email du vendeur de XMR. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.moneyGram=Veuillez s''il vous plaît payer {0} au vendeur de XMR en utilisant MoneyGram.\n\n -portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d''autorisation et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. +portfolio.pending.step2_buyer.moneyGram=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant MoneyGram.\n\n +portfolio.pending.step2_buyer.moneyGram.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le numéro d'autorisation et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.westernUnion=Veuillez s''il vous plaît payer {0} au vendeur de XMR en utilisant Western Union.\n\n -portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}. +portfolio.pending.step2_buyer.westernUnion=Veuillez s'il vous plaît payer {0} au vendeur de XMR en utilisant Western Union.\n\n +portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès avoir effectué le paiement envoyez le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.postal=Merci d''envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n +portfolio.pending.step2_buyer.postal=Merci d'envoyer {0} par \"US Postal Money Order\" au vendeur de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Veuillez envoyer {0} en utlisant \"Pay by Mail\" au vendeur de XMR. Les instructions spécifiques sont dans le contrat de trade, ou si ce n'est pas clair, vous pouvez poser des questions via le chat des trader. Pour plus de détails sur Pay by Mail, allez sur le wiki Haveno \n[LIEN:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Veuillez payer {0} via la méthode de paiement spécifiée par le vendeur de XMR. Vous trouverez les informations du compte du vendeur à l'écran suivant.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.f2f=Veuillez s''il vous plaît contacter le vendeur de XMR via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n +portfolio.pending.step2_buyer.f2f=Veuillez s'il vous plaît contacter le vendeur de XMR via le contact fourni, et planifiez un rendez-vous pour effectuer le paiement {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Initier le paiement en utilisant {0} portfolio.pending.step2_buyer.recipientsAccountData=Destinataires {0} portfolio.pending.step2_buyer.amountToTransfer=Montant à transférer @@ -654,16 +652,16 @@ portfolio.pending.step2_buyer.sellersAddress=Adresse {0} du vendeur portfolio.pending.step2_buyer.buyerAccount=Votre compte de paiement à utiliser portfolio.pending.step2_buyer.paymentSent=Paiement initié portfolio.pending.step2_buyer.fillInBsqWallet=Payer depuis le portefeuille BSQ -portfolio.pending.step2_buyer.warn=Vous n''avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l''échange doit être achevé avant {1}. +portfolio.pending.step2_buyer.warn=Vous n'avez toujours pas effectué votre {0} paiement !\nVeuillez noter que l'échange doit être achevé avant {1}. portfolio.pending.step2_buyer.openForDispute=Vous n'avez pas effectué votre paiement !\nLe délai maximal alloué pour l'échange est écoulé, veuillez contacter le médiateur pour obtenir de l'aide. portfolio.pending.step2_buyer.paperReceipt.headline=Avez-vous envoyé le reçu papier au vendeur de XMR? portfolio.pending.step2_buyer.paperReceipt.msg=Rappelez-vous: \nVous devez écrire sur le reçu papier: PAS DE REMBOURSEMENT.\nEnsuite, veuillez le déchirer en 2, faire une photo et l'envoyer à l'adresse email du vendeur. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Envoyer le numéro d'autorisation ainsi que le reçu -portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d''autorisation et une photo du reçu par email au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l''état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d''autorisation et le contrat au vendeur ? +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Vous devez envoyez le numéro d'autorisation et une photo du reçu par email au vendeur de XMR.\nLe reçu doit faire clairement figurer le nom complet du vendeur, son pays, l'état, et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le numéro d'autorisation et le contrat au vendeur ? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Envoyer le MTCN et le reçu -portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de XMR.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l''état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Vous devez envoyez le MTCN (numéro de suivi) et une photo du reçu par email au vendeur de XMR.\nLe reçu doit clairement faire figurer le nom complet du vendeur, son pays, l'état et le montant. Le mail du vendeur est: {0}.\n\nAvez-vous envoyé le MTCN et le contrat au vendeur ? portfolio.pending.step2_buyer.halCashInfo.headline=Envoyer le code HalCash -portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l''ID de la transaction ({0}) au vendeur de XMR.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? +portfolio.pending.step2_buyer.halCashInfo.msg=Vous devez envoyez un message au format texte SMS avec le code HalCash ainsi que l'ID de la transaction ({0}) au vendeur de XMR.\nLe numéro de mobile du vendeur est {1}.\n\nAvez-vous envoyé le code au vendeur ? portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Certaines banques pourraient vérifier le nom du receveur. Des comptes de paiement plus rapides créés dans des clients Haveno plus anciens ne fournissent pas le nom du receveur, veuillez donc utiliser le chat de trade pour l'obtenir (si nécessaire). portfolio.pending.step2_buyer.confirmStart.headline=Confirmez que vous avez initié le paiement portfolio.pending.step2_buyer.confirmStart.msg=Avez-vous initié le {0} paiement auprès de votre partenaire de trading? @@ -674,10 +672,10 @@ portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=La sasie n'est pas portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorer et continuer tout de même portfolio.pending.step2_seller.waitPayment.headline=En attende du paiement portfolio.pending.step2_seller.f2fInfo.headline=Coordonnées de l'acheteur -portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l''acheteur de XMR lance le {0} payment. -portfolio.pending.step2_seller.warn=L''acheteur de XMR n''a toujours pas effectué le paiement {0}.\nVeuillez attendre qu''il effectue celui-ci.\nSi la transaction n''est pas effectuée le {1}, un arbitre enquêtera. +portfolio.pending.step2_seller.waitPayment.msg=La transaction de dépôt a été vérifiée au moins une fois sur la blockchain\nVous devez attendre que l'acheteur de XMR lance le {0} payment. +portfolio.pending.step2_seller.warn=L'acheteur de XMR n'a toujours pas effectué le paiement {0}.\nVeuillez attendre qu'il effectue celui-ci.\nSi la transaction n'est pas effectuée le {1}, un arbitre enquêtera. portfolio.pending.step2_seller.openForDispute=L'acheteur de XMR n'a pas initié son paiement !\nLa période maximale autorisée pour ce trade est écoulée.\nVous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. -tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l''ID ''{0}'' +tradeChat.chatWindowTitle=Fenêtre de discussion pour la transaction avec l'ID '{0}' tradeChat.openChat=Ouvrir une fenêtre de discussion tradeChat.rules=Vous pouvez communiquer avec votre pair de trading pour résoudre les problèmes potentiels liés à cet échange.\nIl n'est pas obligatoire de répondre sur le chat.\nSi un trader enfreint l'une des règles ci-dessous, ouvrez un litige et signalez-le au médiateur ou à l'arbitre.\n\nRègles sur le chat:\n\t● N'envoyez pas de liens (risque de malware). Vous pouvez envoyer l'ID de transaction et le nom d'un explorateur de blocs.\n\t● N'envoyez pas les mots de votre seed, clés privées, mots de passe ou autre information sensible !\n\t● N'encouragez pas le trading en dehors de Haveno (non sécurisé).\n\t● Ne vous engagez dans aucune forme d'escroquerie d'ingénierie sociale.\n\t● Si un pair ne répond pas et préfère ne pas communiquer par chat, respectez sa décision.\n\t● Limitez la portée de la conversation à l'échange en cours. Ce chat n'est pas une alternative à messenger ou une troll-box.\n\t● Entretenez une conversation amicale et respectueuse. @@ -699,25 +697,25 @@ portfolio.pending.step3_buyer.wait.info=En attente de la confirmation du vendeur portfolio.pending.step3_buyer.wait.msgStateInfo.label=État du message de lancement du paiement portfolio.pending.step3_buyer.warn.part1a=sur la {0} blockchain portfolio.pending.step3_buyer.warn.part1b=chez votre prestataire de paiement (par ex. banque) -portfolio.pending.step3_buyer.warn.part2=Le vendeur de XMR n''a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l''envoi du paiement a bien fonctionné. +portfolio.pending.step3_buyer.warn.part2=Le vendeur de XMR n'a toujours pas confirmé votre paiement. . Veuillez vérifier {0} si l'envoi du paiement a bien fonctionné. portfolio.pending.step3_buyer.openForDispute=Le vendeur de XMR n'a pas confirmé votre paiement ! Le délai maximal alloué pour ce trade est écoulé. Vous pouvez attendre plus longtemps et accorder plus de temps à votre pair de trading ou contacter le médiateur pour obtenir de l'aide. # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu''il a initié le paiement {0}.\n +portfolio.pending.step3_seller.part=Votre partenaire de trading a confirmé qu'il a initié le paiement {0}.\n portfolio.pending.step3_seller.crypto.explorer=Sur votre explorateur blockchain {0} favori portfolio.pending.step3_seller.crypto.wallet=Dans votre portefeuille {0} -portfolio.pending.step3_seller.crypto={0}Veuillez s''il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l''écran principal après avoir fermé ce popup. +portfolio.pending.step3_seller.crypto={0}Veuillez s'il vous plaît vérifier {1} que la transaction vers votre adresse de réception\n{2}\ndispose de suffisamment de confirmations sur la blockchain.\nLe montant du paiement doit être {3}\n\nVous pouvez copier & coller votre adresse {4} à partir de l'écran principal après avoir fermé ce popup. portfolio.pending.step3_seller.postal={0}Veuillez vérifier si vous avez reçu {1} avec \"US Postal Money Order\" de la part de l'acheteur de XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.payByMail={0}Veuillez vérifier si vous avez reçu {1} avec \"Pay by Mail\" de la part de l'acheteur de XMR # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Votre partenaire de trading a confirmé qu'il a initié le {0} paiement.\n\nVeuillez vous rendre sur votre banque en ligne et vérifier si vous avez reçu {1} de la part de l'acheteur de XMR. -portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l''acheteur de XMR doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n''êtes pas sûr, {0} +portfolio.pending.step3_seller.cash=Du fait que le paiement est réalisé via Cash Deposit l'acheteur de XMR doit inscrire \"NO REFUND\" sur le reçu papier, le déchirer en 2 et vous envoyer une photo par email.\n\nPour éviter un risque de rétrofacturation, ne confirmez que si vous recevez le mail et que vous êtes sûr que le reçu papier est valide.\nSi vous n'êtes pas sûr, {0} portfolio.pending.step3_seller.moneyGram=L'acheteur doit vous envoyer le numéro d'autorisation et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier que vous avez bien reçu par e-mail le numéro d'autorisation.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis MoneyGram.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.westernUnion=L'acheteur doit vous envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail .\nLe reçu doit faire clairement figurer votre nom complet, votre pays, l'état et le montant. Veuillez s'il vous plaît vérifier si vous avez reçu par e-mail le MTCN.\n\nAprès avoir fermé ce popup vous verrez le nom de l'acheteur de XMR et l'adresse où retirer l'argent depuis Western Union.\n\nN'accusez réception qu'après avoir retiré l'argent avec succès! portfolio.pending.step3_seller.halCash=L'acheteur doit vous envoyer le code HalCash par message texte SMS. Par ailleurs, vous recevrez un message de la part d'HalCash avec les informations nécessaires pour retirer les EUR depuis un DAB Bancaire supportant HalCash.\n\nAprès avoir retiré l'argent au DAB, veuillez confirmer ici la réception du paiement ! portfolio.pending.step3_seller.amazonGiftCard=L'acheteur vous a envoyé une e-carte cadeau Amazon via email ou SMS vers votre téléphone. Veuillez récupérer maintenant la carte cadeau sur votre compte Amazon, et une fois activée, confirmez le reçu de paiement. -portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, associé au contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} +portfolio.pending.step3_seller.bankCheck=\n\nVeuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, associé au contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Confirmer la réception du paiement @@ -729,17 +727,17 @@ portfolio.pending.step3_seller.xmrTxHash=ID de la transaction portfolio.pending.step3_seller.xmrTxKey=Clé de Transaction portfolio.pending.step3_seller.buyersAccount=Données du compte de l'acheteur portfolio.pending.step3_seller.confirmReceipt=Confirmer la réception du paiement -portfolio.pending.step3_seller.buyerStartedPayment=L''acheteur XMR a commencé le {0} paiement.\n{1} +portfolio.pending.step3_seller.buyerStartedPayment=L'acheteur XMR a commencé le {0} paiement.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.crypto=Vérifiez la présence de confirmations par la blockchain dans votre portefeuille crypto ou sur un explorateur de blocs et confirmez le paiement lorsque vous aurez suffisamment de confirmations sur la blockchain. portfolio.pending.step3_seller.buyerStartedPayment.traditional=Vérifiez sur votre compte de trading (par ex. compte bancaire) et confirmez quand vous avez reçu le paiement. portfolio.pending.step3_seller.warn.part1a=sur la {0} blockchain portfolio.pending.step3_seller.warn.part1b=Auprès de votre prestataire de paiement (par ex. banque) -portfolio.pending.step3_seller.warn.part2=Vous n''avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. +portfolio.pending.step3_seller.warn.part2=Vous n'avez toujours pas confirmé la réception du paiement. Veuillez vérifier {0} si vous avez reçu le paiement. portfolio.pending.step3_seller.openForDispute=Vous n'avez pas confirmé la réception du paiement !\nLe délai maximal alloué pour ce trade est écoulé.\nVeuillez confirmer ou demander l'aide du médiateur. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Avez-vous reçu le paiement {0} de votre partenaire de trading?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l''expéditeur indiqué sur le contrat de l''échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l''expéditeur, avec le contrat de l''échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Veuillez également vérifier que le nom de l'expéditeur indiqué sur le contrat de l'échange correspond au nom qui apparaît sur votre relevé bancaire:\nNom de l'expéditeur, avec le contrat de l'échange: {0}\n\nSi les noms ne sont pas exactement identiques, ne confirmez pas la réception du paiement. Au lieu de cela, ouvrez un litige en appuyant sur \"alt + o\" ou \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Veuillez noter que dès que vous aurez confirmé la réception, le montant verrouillé pour l'échange sera remis à l'acheteur de XMR et le dépôt de garantie vous sera remboursé.\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirmez que vous avez bien reçu le paiement @@ -778,14 +776,14 @@ portfolio.pending.remainingTime=Temps restant portfolio.pending.remainingTimeDetail={0} (jusqu'’à {1}) portfolio.pending.tradePeriodInfo=Après la première confirmation de la blockchain, la période de trade commence. En fonction de la méthode de paiement utilisée, une période maximale allouée pour la transaction sera appliquée. portfolio.pending.tradePeriodWarning=Si le délai est dépassé, l'es deux participants du trade peuvent ouvrir un litige. -portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu''à {0}) +portfolio.pending.tradeNotCompleted=Trade inachevé dans le temps imparti (jusqu'à {0}) portfolio.pending.tradeProcess=Processus de transaction portfolio.pending.openAgainDispute.msg=Si vous n'êtes pas certain que le message addressé au médiateur ou à l'arbitre soit arrivé (par exemple si vous n'avez pas reçu de réponse dans un délai de 1 jour), n'hésitez pas à réouvrir un litige avec Cmd/ctrl+O. Vous pouvez aussi demander de l'aide en complément sur le forum haveno à [LIEN:https://haveno.community]. portfolio.pending.openAgainDispute.button=Ouvrir à nouveau le litige portfolio.pending.openSupportTicket.headline=Ouvrir un ticket d'assistance portfolio.pending.openSupportTicket.msg=S'il vous plaît n'utilisez seulement cette fonction qu'en cas d'urgence si vous ne pouvez pas voir le bouton \"Open support\" ou \"Ouvrir un litige\.\n\nLorsque vous ouvrez un ticket de support, l'échange sera interrompu et pris en charge par le médiateur ou par l'arbitre. -portfolio.pending.timeLockNotOver=Vous devez patienter jusqu''au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. +portfolio.pending.timeLockNotOver=Vous devez patienter jusqu'au ≈{0} ({1} blocs de plus) avant de pouvoir ouvrir ouvrir un arbitrage pour le litige. portfolio.pending.error.depositTxNull=La transaction de dépôt est nulle. Vous ne pouvez pas ouvrir un litige sans une transaction de dépôt valide. Allez dans \"Paramètres/Info sur le réseau\" et faites une resynchronisation SPV.\n\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. portfolio.pending.mediationResult.error.depositTxNull=La transaction de dépôt est nulle. Vous pouvez déplacer le trade vers les trades n'ayant pas réussi. portfolio.pending.mediationResult.error.delayedPayoutTxNull=Le paiement de la transaction différée est nul. Vous pouvez déplacer le trade vers les trades échoués. @@ -796,6 +794,8 @@ portfolio.pending.support.text.getHelp=Si vous rencontrez des problèmes, vous p portfolio.pending.support.button.getHelp=Ouvrir le chat de trade portfolio.pending.support.headline.halfPeriodOver=Vérifier le paiement portfolio.pending.support.headline.periodOver=Le délai alloué pour ce trade est écoulé. +portfolio.pending.support.headline.depositTxMissing=Transaction de dépôt manquante +portfolio.pending.support.depositTxMissing=Une transaction de dépôt est manquante pour cette opération. Ouvrez un ticket de support pour contacter un arbitre et obtenir de l’aide. portfolio.pending.mediationRequested=Médiation demandée portfolio.pending.refundRequested=Remboursement demandé @@ -812,7 +812,7 @@ portfolio.pending.mediationResult.info.noneAccepted=Terminez la transaction en a portfolio.pending.mediationResult.info.selfAccepted=Vous avez accepté la suggestion du médiateur. En attente que le pair l'accepte également. portfolio.pending.mediationResult.info.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur. L'acceptez-vous également ? portfolio.pending.mediationResult.button=Voir la résolution proposée -portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l''ID: {0} +portfolio.pending.mediationResult.popup.headline=Résultat de la médiation pour la transaction avec l'ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Votre pair de trading a accepté la suggestion du médiateur pour la transaction {0} portfolio.pending.mediationResult.popup.info=Les frais recommandés par le médiateur sont les suivants: \nVous paierez: {0} \nVotre partenaire commercial paiera: {1} \n\nVous pouvez accepter ou refuser ces frais de médiation. \n\nEn acceptant, vous avez vérifié l'opération de paiement du contrat. Si votre partenaire commercial accepte et vérifie également, le paiement sera effectué et la transaction sera clôturée. \n\nSi l'un de vous ou les deux refusent la proposition, vous devrez attendre le {2} (bloc {3}) pour commencer le deuxième tour de discussion sur le différend avec l'arbitre, et ce dernier étudiera à nouveau le cas. Le paiement sera fait en fonction de ses résultats. \n\nL'arbitre peut facturer une somme modique (la limite supérieure des honoraires: la marge de la transaction) en compensation de son travail. Les deux commerçants conviennent que la suggestion du médiateur est une voie agréable. La demande d'arbitrage concerne des circonstances particulières, par exemple si un professionnel est convaincu que le médiateur n'a pas fait une recommandation de d'indemnisation équitable (ou si l'autre partenaire n'a pas répondu). \n\nPlus de détails sur le nouveau modèle d'arbitrage: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Vous avez accepté la proposition de paiement du médiateur, mais il semble que votre contrepartie ne l'ait pas acceptée. \n\nUne fois que le temps de verrouillage atteint {0} (bloc {1}), vous pouvez ouvrir le second tour de litige pour que l'arbitre réétudie le cas et prend une nouvelle décision de dépenses. \n\nVous pouvez trouver plus d'informations sur le modèle d'arbitrage sur:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] @@ -821,7 +821,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Vous avez déjà accept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Le frais de transaction du preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés et aucun frais de trade a été payé. Vous pouvez déplacer ce trade vers les trade échoués. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Le frais de transaction du pair preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés. Votre offre est toujours valable pour les autres traders, vous n'avez donc pas perdu le frais de maker. Vous pouvez déplacer ce trade vers les trades échoués. -portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. +portfolio.pending.failedTrade.missingDepositTx=Une transaction de dépôt est manquante.\n\nCette transaction est nécessaire pour compléter la transaction. Veuillez vous assurer que votre portefeuille est entièrement synchronisé avec la blockchain Monero.\n\nVous pouvez déplacer cette transaction dans la section « Transactions échouées » pour la désactiver. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/haveno-dex/haveno/issues] @@ -892,10 +892,10 @@ funds.withdrawal.warn.noSourceAddressSelected=Vous devez sélectionner une adres funds.withdrawal.warn.amountExceeds=Vous ne disposez pas de fonds suffisants provenant de l'adresse sélectionnée.\nEnvisagez de sélectionner plusieurs adresses dans le champ ci-dessus ou changez les frais pour inclure les frais du mineur. funds.reserved.noFunds=Aucun fonds n'est réservé pour les ordres en cours -funds.reserved.reserved=Réversé dans votre portefeuille local pour l''ordre avec l''ID: {0} +funds.reserved.reserved=Réversé dans votre portefeuille local pour l'ordre avec l'ID: {0} funds.locked.noFunds=Aucun fonds n'est verrouillé dans les trades -funds.locked.locked=Vérouillé en multisig pour le trade avec l''ID: {0} +funds.locked.locked=Vérouillé en multisig pour le trade avec l'ID: {0} funds.tx.direction.sentTo=Envoyer à: funds.tx.direction.receivedWith=Reçu depuis: @@ -908,7 +908,7 @@ funds.tx.disputePayout=Versement du litige: {0} funds.tx.disputeLost=Cas de litige perdu: {0} funds.tx.collateralForRefund=Remboursement du dépôt de garantie: {0} funds.tx.timeLockedPayoutTx=Tx de paiement verrouillée dans le temps: {0} -funds.tx.refund=Remboursement venant de l''arbitrage: {0} +funds.tx.refund=Remboursement venant de l'arbitrage: {0} funds.tx.unknown=Raison inconnue: {0} funds.tx.noFundsFromDispute=Aucun remboursement en cas de litige funds.tx.receivedFunds=Fonds reçus @@ -929,8 +929,6 @@ support.tab.mediation.support=Médiation support.tab.arbitration.support=Arbitrage support.tab.legacyArbitration.support=Conclusion d'arbitrage support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Chercher les litiges -support.filter.prompt=Saisissez l'ID du trade, la date, l'adresse "onion" ou les données du compte. support.sigCheck.button=Vérifier la signature support.sigCheck.popup.info=Collez le message récapitulatif du processus d'arbitrage. Avec cet outil, n'importe quel utilisateur peut vérifier si la signature de l'arbitre correspond au message récapitulatif. @@ -950,9 +948,9 @@ support.fullReportButton.label=Tous les litiges support.noTickets=Il n'y a pas de tickets ouverts support.sendingMessage=Envoi du message... support.receiverNotOnline=Le destinataire n'est pas en ligne. Le message est enregistré dans leur boîte mail. -support.sendMessageError=Échec de l''envoi du message. Erreur: {0} +support.sendMessageError=Échec de l'envoi du message. Erreur: {0} support.receiverNotKnown=Destinataire inconnu -support.wrongVersion=L''ordre relatif au litige en question a été créé avec une ancienne version de Haveno.\nVous ne pouvez pas clore ce litige avec votre version de l''application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} +support.wrongVersion=L'ordre relatif au litige en question a été créé avec une ancienne version de Haveno.\nVous ne pouvez pas clore ce litige avec votre version de l'application.\n\nVeuillez utiliser une version plus ancienne avec la version du protocole {0} support.openFile=Ouvrir le fichier à joindre (taille max. du fichier : {0} kb) support.attachmentTooLarge=La taille totale de vos pièces jointes est de {0} ko ce qui dépasse la taille maximale autorisée de {1} ko pour les messages. support.maxSize=La taille maximale autorisée pour le fichier est {0} kB. @@ -968,7 +966,7 @@ support.attachments=Pièces jointes: support.savedInMailbox=Message sauvegardé dans la boîte mail du destinataire support.arrived=Message reçu par le destinataire support.acknowledged=Réception du message confirmée par le destinataire -support.error=Le destinataire n''a pas pu traiter le message. Erreur : {0} +support.error=Le destinataire n'a pas pu traiter le message. Erreur : {0} support.buyerAddress=Adresse de l'acheteur XMR support.sellerAddress=Adresse du vendeur XMR support.role=Rôle @@ -984,7 +982,7 @@ support.buyerTaker=Acheteur XMR/Taker support.sellerTaker=Vendeur XMR/Taker support.backgroundInfo=Haveno n'est pas une entreprise, donc il gère les litiges différemment.\n\nLes traders peuvent communiquer au sein de l'application via une discussion sécurisée sur l'écran des transactions ouvertes pour tenter de résoudre les litiges eux-mêmes. Si cela n'est pas suffisant, un médiateur évaluera la situation et décidera d'un paiement des fonds de transaction. -support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d''informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l''acheteur XMR : Avez-vous effectué le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l''application ?\n● Si vous êtes le vendeur XMR : Avez-vous reçu le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l''application ?\n● Quelle version de Haveno utilisez-vous ?\n● Quel système d''exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu''ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l''accord d''utilisation lorsque vous avez lancé l''application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l''adresse suivante {2} +support.initialInfo=Veuillez entrer une description de votre problème dans le champ texte ci-dessous. Ajoutez autant d'informations que possible pour accélérer le temps de résolution du litige.\n\nVoici une check list des informations que vous devez fournir :\n● Si vous êtes l'acheteur XMR : Avez-vous effectué le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement commencé" dans l'application ?\n● Si vous êtes le vendeur XMR : Avez-vous reçu le paiement Fiat ou Crypto ? Si oui, avez-vous cliqué sur le bouton "paiement reçu" dans l'application ?\n● Quelle version de Haveno utilisez-vous ?\n● Quel système d'exploitation utilisez-vous ?\n● Si vous avez rencontré un problème avec des transactions qui ont échoué, veuillez envisager de passer à un nouveau répertoire de données.\nParfois, le répertoire de données est corrompu et conduit à des bogues étranges. \nVoir : https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nVeuillez vous familiariser avec les règles de base du processus de règlement des litiges :\n● Vous devez répondre aux demandes des {0} dans les 2 jours.\n● Les médiateurs répondent dans un délai de 2 jours. Les arbitres répondent dans un délai de 5 jours ouvrables.\n● Le délai maximum pour un litige est de 14 jours.\n● Vous devez coopérer avec les {1} et fournir les renseignements qu'ils demandent pour faire valoir votre cause.\n● Vous avez accepté les règles décrites dans le document de litige dans l'accord d'utilisation lorsque vous avez lancé l'application pour la première fois.\n\nVous pouvez en apprendre davantage sur le processus de litige à l'adresse suivante {2} support.systemMsg=Message du système: {0} support.youOpenedTicket=Vous avez ouvert une demande de support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=Vous avez ouvert une demande de litige.\n\n{0}\n\nHaveno version: {1} @@ -1037,6 +1035,7 @@ setting.preferences.displayOptions=Afficher les options setting.preferences.showOwnOffers=Montrer mes ordres dans le livre des ordres setting.preferences.useAnimations=Utiliser des animations setting.preferences.useDarkMode=Utiliser le mode sombre +setting.preferences.useLightMode=Utiliser le mode clair setting.preferences.sortWithNumOffers=Trier les listes de marché avec le nombre d'ordres/de transactions setting.preferences.onlyShowPaymentMethodsFromAccount=Masquer les méthodes de paiement non supportées setting.preferences.denyApiTaker=Refuser les preneurs utilisant l'API @@ -1052,6 +1051,9 @@ settings.preferences.editCustomExplorer.name=Nom settings.preferences.editCustomExplorer.txUrl=URL de la transaction settings.preferences.editCustomExplorer.addressUrl=Addresse URL +setting.info.headline=Nouvelle fonctionnalité de confidentialité des données +settings.preferences.sensitiveDataRemoval.msg=Pour protéger la vie privée de vous-même et des autres traders, Haveno a l'intention de supprimer les données sensibles des anciennes transactions. Cela est particulièrement important pour les transactions en fiat qui peuvent inclure des informations bancaires.\n\nIl est recommandé de régler ce délai aussi bas que possible, par exemple 60 jours. Cela signifie que les transactions datant de plus de 60 jours verront leurs données sensibles supprimées, tant qu'elles sont terminées. Les transactions terminées se trouvent dans l'onglet Portefeuille / Historique. + settings.net.xmrHeader=Réseau Monero settings.net.p2pHeader=Le réseau Haveno settings.net.onionAddressLabel=Mon adresse onion @@ -1118,22 +1120,22 @@ setting.about.subsystems.label=Versions des sous-systèmes setting.about.subsystems.val=Version du réseau: {0}; version des messages P2P: {1}; Version DB Locale: {2}; Version du protocole de trading: {3} setting.about.shortcuts=Raccourcis -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Naviguer dans le menu principal setting.about.shortcuts.menuNav.value=Pour naviguer dans le menu principal, appuyez sur: 'Ctrl' ou 'alt' ou 'cmd' avec une touche numérique entre '1-9'. setting.about.shortcuts.close=Fermer Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fermer le popup ou la fenêtre de dialogue setting.about.shortcuts.closePopup.value=Touche 'ECHAP' setting.about.shortcuts.chatSendMsg=Envoyer un message chat au trader -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTRÉE'' ou ''alt + ENTREE'' ou ''cmd + ENTRÉE'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTRÉE' ou 'alt + ENTREE' ou 'cmd + ENTRÉE' setting.about.shortcuts.openDispute=Ouvrir un litige -setting.about.shortcuts.openDispute.value=Sélectionnez l''échange en cours et cliquez sur: {0} +setting.about.shortcuts.openDispute.value=Sélectionnez l'échange en cours et cliquez sur: {0} setting.about.shortcuts.walletDetails=Ouvrir la fenêtre avec les détails sur le portefeuille @@ -1153,7 +1155,7 @@ setting.about.shortcuts.registerMediator=Inscrire le médiateur (médiateur/arbi setting.about.shortcuts.registerMediator.value=Naviguez jusqu'au compte et appuyez sur: {0} setting.about.shortcuts.openSignPaymentAccountsWindow=Ouvrir la fenêtre pour la signature de l'âge du compte (anciens arbitres seulement) -setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l''ancienne vue de l''arbitre et appuyer sur: {0} +setting.about.shortcuts.openSignPaymentAccountsWindow.value=Naviguer vers l'ancienne vue de l'arbitre et appuyer sur: {0} setting.about.shortcuts.sendAlertMsg=Envoyer un message d'alerte ou de mise à jour (activité privilégiée) @@ -1197,15 +1199,15 @@ account.arbitratorRegistration.pubKey=Clé publique account.arbitratorRegistration.register=S'inscrire account.arbitratorRegistration.registration={0} Enregistrement account.arbitratorRegistration.revoke=Révoquer -account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d''échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu''à 7 jours. +account.arbitratorRegistration.info.msg=Veuillez noter que vous devez rester disponible pendant 15 jours après la révocation, car il se peut que des échanges vous impliquent comme {0}. Le délai d'échange maximal autorisé est de 8 jours et la procédure de contestation peut prendre jusqu'à 7 jours. account.arbitratorRegistration.warn.min1Language=Vous devez définir au moins 1 langue.\nNous avons ajouté la langue par défaut pour vous. account.arbitratorRegistration.removedSuccess=Vous avez supprimé votre inscription au réseau Haveno avec succès. -account.arbitratorRegistration.removedFailed=Impossible de supprimer l''enregistrement.{0} +account.arbitratorRegistration.removedFailed=Impossible de supprimer l'enregistrement.{0} account.arbitratorRegistration.registerSuccess=Vous vous êtes inscrit au réseau Haveno avec succès. -account.arbitratorRegistration.registerFailed=Impossible de terminer l''enregistrement.{0} +account.arbitratorRegistration.registerFailed=Impossible de terminer l'enregistrement.{0} account.crypto.yourCryptoAccounts=Vos comptes crypto -account.crypto.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l''utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL''utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d''un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l''arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. +account.crypto.popup.wallet.msg=Veuillez vous assurer que vous respectez les exigences relatives à l'utilisation des {0} portefeuilles, selon les conditions présentées sur la page {1} du site.\nL'utilisation des portefeuilles provenant de plateformes de trading centralisées où (a) vous ne contrôlez pas vos clés ou (b) qui ne disposent pas d'un portefeuille compatible est risquée : cela peut entraîner la perte des fonds échangés!\nLe médiateur et l'arbitre ne sont pas des spécialistes {2} et ne pourront pas intervenir dans ce cas. account.crypto.popup.wallet.confirm=Je comprends et confirme que je sais quel portefeuille je dois utiliser. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Pour échanger UPX sur Haveno, vous devez comprendre et respecter les exigences suivantes: \n\nPour envoyer UPX, vous devez utiliser le portefeuille officiel UPXmA GUI ou le portefeuille UPXmA CLI avec le logo store-tx-info activé (valeur par défaut dans la nouvelle version) . Assurez-vous d'avoir accès à la clé tx, car elle est nécessaire dans l'état du litige. monero-wallet-cli (à l'aide de la commande get_Tx_key) monero-wallet-gui: sur la page Avancé> Preuve / Vérification. \n\nCes transactions ne sont pas vérifiables dans le navigateur blockchain ordinaire. \n\nEn cas de litige, vous devez fournir à l'arbitre les informations suivantes: \n\n- Clé privée Tx- hachage de transaction- adresse publique du destinataire \n\nSi vous ne fournissez pas les informations ci-dessus ou si vous utilisez un portefeuille incompatible, vous perdrez le litige. En cas de litige, l'expéditeur UPX est responsable de fournir la vérification du transfert UPX à l'arbitre. \n\nAucun paiement d'identité n'est requis, juste une adresse publique commune. \n\nSi vous n'êtes pas sûr du processus, veuillez visiter le canal UPXmA Discord (https://discord.gg/vhdNSrV) ou le groupe d'échanges Telegram (https://t.me/uplexaOfficial) pour plus d'informations. @@ -1251,8 +1253,8 @@ account.backup.backupNow=Sauvegarder maintenant (la sauvegarde n'est pas crypté account.backup.appDir=Répertoire des données de l'application account.backup.openDirectory=Ouvrir le répertoire account.backup.openLogFile=Ouvrir le fichier de log -account.backup.success=Sauvegarder réussite vers l''emplacement:\n{0} -account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n''est pas accessible. {0} +account.backup.success=Sauvegarder réussite vers l'emplacement:\n{0} +account.backup.directoryNotAccessible=Le répertoire que vous avez choisi n'est pas accessible. {0} account.password.removePw.button=Supprimer le mot de passe account.password.removePw.headline=Supprimer la protection par mot de passe du portefeuille @@ -1296,13 +1298,13 @@ account.notifications.priceAlert.low.label=Me prévenir si le prix du XMR est in account.notifications.priceAlert.setButton=Définir l'alerte de prix account.notifications.priceAlert.removeButton=Retirer l'alerte de prix account.notifications.trade.message.title=L'état du trade a été modifié. -account.notifications.trade.message.msg.conf=La transaction de dépôt pour l''échange avec ID {0} est confirmée. Veuillez ouvrir votre application Haveno et initier le paiement. -account.notifications.trade.message.msg.started=L''acheteur de XMR a initié le paiement pour la transaction avec ID {0}. -account.notifications.trade.message.msg.completed=La transaction avec l''ID {0} est terminée. +account.notifications.trade.message.msg.conf=La transaction de dépôt pour l'échange avec ID {0} est confirmée. Veuillez ouvrir votre application Haveno et initier le paiement. +account.notifications.trade.message.msg.started=L'acheteur de XMR a initié le paiement pour la transaction avec ID {0}. +account.notifications.trade.message.msg.completed=La transaction avec l'ID {0} est terminée. account.notifications.offer.message.title=Votre ordre a été accepté -account.notifications.offer.message.msg=Votre ordre avec l''ID {0} a été accepté +account.notifications.offer.message.msg=Votre ordre avec l'ID {0} a été accepté account.notifications.dispute.message.title=Nouveau message de litige -account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l''ID {0} +account.notifications.dispute.message.msg=Vous avez reçu un message de contestation pour le trade avec l'ID {0} account.notifications.marketAlert.title=Alertes sur les ordres account.notifications.marketAlert.selectPaymentAccount=Ordres correspondants au compte de paiement @@ -1321,9 +1323,9 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Type d'ordre account.notifications.marketAlert.message.title=Alerte d'ordre account.notifications.marketAlert.message.msg.below=en dessous de account.notifications.marketAlert.message.msg.above=au dessus de -account.notifications.marketAlert.message.msg=Un nouvel ordre ''{0} {1}''' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement ''{5}'' a été publiée dans le livre des ordres de Haveno.\nID de l''ordre: {6}. +account.notifications.marketAlert.message.msg=Un nouvel ordre '{0} {1}'' avec le prix {2} ({3} {4} prix de marché) avec le moyen de paiement '{5}' a été publiée dans le livre des ordres de Haveno.\nID de l'ordre: {6}. account.notifications.priceAlert.message.title=Alerte de prix pour {0} -account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l''actuel {0} le prix est {1}. {2} +account.notifications.priceAlert.message.msg=Votre alerte de prix a été déclenchée. l'actuel {0} le prix est {1}. {2} account.notifications.noWebCamFound.warning=Aucune webcam n'a été trouvée.\n\nUtilisez l'option mail pour envoyer le jeton et la clé de cryptage depuis votre téléphone portable vers l'application Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Le prix le plus élevé doit être supérieur au prix le plus bas. account.notifications.priceAlert.warning.lowerPriceTooHigh=Le prix le plus bas doit être inférieur au prix le plus élevé. @@ -1419,9 +1421,9 @@ disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nÉtape suivant disputeSummaryWindow.close.closePeer=Vous devez également clore le ticket des pairs de trading ! disputeSummaryWindow.close.txDetails.headline=Publier la transaction de remboursement # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.txDetails.buyer=L''acheteur reçoit {0} à l''adresse: {1}\n +disputeSummaryWindow.close.txDetails.buyer=L'acheteur reçoit {0} à l'adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l''adresse: {1}\n +disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l'adresse: {1}\n disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3}\n\nÊtes-vous sûr de vouloir publier cette transaction ? disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement @@ -1474,11 +1476,14 @@ offerDetailsWindow.countryBank=Pays de la banque du Maker offerDetailsWindow.commitment=Engagement offerDetailsWindow.agree=J'accepte offerDetailsWindow.tac=Conditions d'utilisation -offerDetailsWindow.confirm.maker=Confirmer: Placer un ordre de {0} monero -offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} monero +offerDetailsWindow.confirm.maker.buy=Confirmer : Créer une offre pour acheter XMR avec {0} +offerDetailsWindow.confirm.maker.sell=Confirmer : Créer une offre pour vendre XMR contre {0} +offerDetailsWindow.confirm.taker.buy=Confirmer : Accepter une offre pour acheter XMR avec {0} +offerDetailsWindow.confirm.taker.sell=Confirmer : Accepter une offre pour vendre XMR contre {0} offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker offerDetailsWindow.challenge=Phrase secrète de l'offre +offerDetailsWindow.challenge.copy=Copier la phrase secrète à partager avec votre pair qRCodeWindow.headline=QR Code qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Haveno. @@ -1564,7 +1569,7 @@ torNetworkSettingWindow.deleteFiles.button=Supprimer les fichiers Tor obsolètes torNetworkSettingWindow.deleteFiles.progress=Arrêt de Tor en cours torNetworkSettingWindow.deleteFiles.success=Fichiers Tor obsolètes supprimés avec succès. Veuillez redémarrer. torNetworkSettingWindow.bridges.header=Tor est-il bloqué? -torNetworkSettingWindow.bridges.info=Si Tor est bloqué par votre fournisseur Internet ou dans votre pays, vous pouvez essayer d'utiliser les passerelles Tor.\nVisitez la page web de Tor sur: https://bridges.torproject.org/bridges pour en savoir plus sur les bridges et les pluggable transports. +torNetworkSettingWindow.bridges.info=Si Tor est bloqué par votre fournisseur Internet ou dans votre pays, vous pouvez essayer d'utiliser les passerelles Tor.\nVisitez la page web de Tor sur: https://bridges.torproject.org pour en savoir plus sur les bridges et les pluggable transports. feeOptionWindow.headline=Choisissez la devise pour le paiement des frais de transaction feeOptionWindow.info=Vous pouvez choisir de payer les frais de transaction en BSQ ou en XMR. Si vous choisissez BSQ, vous bénéficierez de frais de transaction réduits. @@ -1592,29 +1597,29 @@ popup.headline.error=Erreur popup.doNotShowAgain=Ne plus montrer popup.reportError.log=Ouvrir le dossier de log popup.reportError.gitHub=Signaler au Tracker de problème GitHub -popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d''erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l''un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l''attachant à votre rapport de bug. +popup.reportError={0}\n\nAfin de nous aider à améliorer le logiciel, veuillez signaler ce bug en ouvrant un nouveau ticket de support sur https://github.com/haveno-dex/haveno/issues.\nLe message d'erreur ci-dessus sera copié dans le presse-papier lorsque vous cliquerez sur l'un des boutons ci-dessous.\nCela facilitera le dépannage si vous incluez le fichier haveno.log en appuyant sur "ouvrir le fichier de log", en sauvegardant une copie, et en l'attachant à votre rapport de bug. popup.error.tryRestart=Veuillez essayer de redémarrer votre application et vérifier votre connexion réseau pour voir si vous pouvez résoudre ce problème. -popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu''un essayait d''accepter l''un de vos ordres:\n{0} +popup.error.takeOfferRequestFailed=Une erreur est survenue pendant que quelqu'un essayait d'accepter l'un de vos ordres:\n{0} -error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d''erreur: {0}\n\nVoulez-vous l''effacer et lancer une resynchronisation? +error.spvFileCorrupted=Une erreur est survenue pendant la lecture du fichier de la chaîne SPV.\nIl se peut que le fichier de la chaîne SPV soit corrompu.\n\nMessage d'erreur: {0}\n\nVoulez-vous l'effacer et lancer une resynchronisation? error.deleteAddressEntryListFailed=Impossible de supprimer le dossier AddressEntryList.\nErreur: {0}. -error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l''échange fermé avec l''ID d''échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. -error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l''ID d'échange {0} est nulle.\n\nVeuillez redémarrer l''application pour nettoyer la liste des transactions fermées. +error.closedTradeWithUnconfirmedDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} n'est pas encore confirmée.\n\nVeuillez effectuer une resynchronisation SPV à \"Paramètres/Info sur le réseau\" pour voir si la transaction est valide. +error.closedTradeWithNoDepositTx=La transaction de dépôt de l'échange fermé avec l'ID d'échange {0} est nulle.\n\nVeuillez redémarrer l'application pour nettoyer la liste des transactions fermées. popup.warning.walletNotInitialized=Le portefeuille n'est pas encore initialisé popup.warning.osxKeyLoggerWarning=En raison de mesures de sécurité plus strictes dans MacOS 10.14 et dans la version supérieure, le lancement d'une application Java (Haveno utilise Java) provoquera un avertissement pop-up dans MacOS (« Haveno souhaite recevoir les frappes de toute application »). \n\nPour éviter ce problème, veuillez ouvrir «Paramètres MacOS», puis allez dans «Sécurité et confidentialité» -> «Confidentialité» -> «Surveillance des entrées», puis supprimez «Haveno» de la liste de droite. \n\nUne fois les limitations techniques résolues (le packager Java de la version Java requise n'a pas été livré), Haveno effectuera une mise à niveau vers la nouvelle version Java pour éviter ce problème. -popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Haveno sur cet ordinateur.\nL''architecture de votre ordinateur est: {0}.\nLa binary Haveno que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). +popup.warning.wrongVersion=Vous avez probablement une mauvaise version de Haveno sur cet ordinateur.\nL'architecture de votre ordinateur est: {0}.\nLa binary Haveno que vous avez installé est: {1}.\nVeuillez éteindre et réinstaller une bonne version ({2}). popup.warning.incompatibleDB=Nous avons détecté un fichier de base de données incompatible!\n\nCes fichiers de base de données ne sont pas compatibles avec notre base de code actuelle: {0}\n\nNous avons sauvegardé les fichiers endommagés et appliqué les valeurs par défaut à la nouvelle version de la base de données.\n\nLa sauvegarde se trouve dans: \n\n{1} / db / backup_of_corrupted_data. \n\nVeuillez vérifier si vous avez installé la dernière version de Haveno. \n\nVous pouvez télécharger: \n\n[HYPERLINK:https://haveno.exchange/downloads] \n\nVeuillez redémarrer l'application. popup.warning.startupFailed.twoInstances=Haveno est déjà lancé. Vous ne pouvez pas lancer deux instances de haveno. -popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n''est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l''état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. -popup.warning.tradePeriod.ended=Votre échange avec l''ID {0} a atteint la période de trading maximale autorisée et n''est pas terminé.\n\nLa période d''échange s''est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. +popup.warning.tradePeriod.halfReached=Votre transaction avec ID {0} a atteint la moitié de la période de trading maximale autorisée et n'est toujours pas terminée.\n\nLa période de trade se termine le {1}.\n\nVeuillez vérifier l'état de votre transaction dans \"Portfolio/échanges en cours\" pour obtenir de plus amples informations. +popup.warning.tradePeriod.ended=Votre échange avec l'ID {0} a atteint la période de trading maximale autorisée et n'est pas terminé.\n\nLa période d'échange s'est terminée le {1}.\n\nVeuillez vérifier votre transaction sur \"Portfolio/Echanges en cours\" pour contacter le médiateur. popup.warning.noTradingAccountSetup.headline=Vous n'avez pas configuré de compte de trading popup.warning.noTradingAccountSetup.msg=Vous devez configurer une devise nationale ou un compte crypto avant de pouvoir créer un ordre.\nVoulez-vous configurer un compte ? popup.warning.noArbitratorsAvailable=Les arbitres ne sont pas disponibles. popup.warning.noMediatorsAvailable=Il n'y a pas de médiateurs disponibles. popup.warning.notFullyConnected=Vous devez attendre d'être complètement connecté au réseau.\nCela peut prendre jusqu'à 2 minutes au démarrage. -popup.warning.notSufficientConnectionsToXmrNetwork=Vous devez attendre d''avoir au minimum {0} connexions au réseau Monero. +popup.warning.notSufficientConnectionsToXmrNetwork=Vous devez attendre d'avoir au minimum {0} connexions au réseau Monero. popup.warning.downloadNotComplete=Vous devez attendre que le téléchargement des blocs Monero manquants soit terminé. popup.warning.walletNotSynced=Le portefeuille Haveno n'est pas synchronisé avec la hauteur la plus récente de la blockchain. Veuillez patienter jusqu'à ce que le portefeuille soit synchronisé ou vérifiez votre connexion. popup.warning.removeOffer=Vous êtes certain de vouloir retirer cet ordre? @@ -1623,7 +1628,8 @@ popup.warning.examplePercentageValue=Merci de saisir un nombre sous la forme d'u popup.warning.noPriceFeedAvailable=Il n'y a pas de flux pour le prix de disponible pour cette devise. Vous ne pouvez pas utiliser un prix basé sur un pourcentage.\nVeuillez sélectionner le prix fixé. popup.warning.sendMsgFailed=L'envoi du message à votre partenaire d'échange a échoué.\nMerci d'essayer de nouveau et si l'échec persiste merci de reporter le bug. popup.warning.messageTooLong=Votre message dépasse la taille maximale autorisée. Veuillez l'envoyer en plusieurs parties ou le télécharger depuis un service comme https://pastebin.com. -popup.warning.lockedUpFunds=Vous avez des fonds bloqués d''une transaction qui a échoué.\nSolde bloqué: {0}\nAdresse de la tx de dépôt: {1}\nID de l''échange: {2}.\n\nVeuillez ouvrir un ticket de support en sélectionnant la transaction dans l'écran des transactions ouvertes et en appuyant sur \"alt + o\" ou \"option + o\". +popup.warning.lockedUpFunds=Vous avez des fonds bloqués d'une transaction qui a échoué.\nSolde bloqué: {0}\nAdresse de la tx de dépôt: {1}\nID de l'échange: {2}.\n\nVeuillez ouvrir un ticket de support en sélectionnant la transaction dans l'écran des transactions ouvertes et en appuyant sur \"alt + o\" ou \"option + o\". +popup.warning.moneroConnection=Il y a eu un problème de connexion au réseau Monero.\n\n{0} popup.warning.makerTxInvalid=Cette offre n'est pas valide. Veuillez choisir une autre offre.\n\n takeOffer.cancelButton=Annuler la prise de l'offre @@ -1636,19 +1642,19 @@ popup.warning.priceRelay=Relais de prix popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Veuillez faire une mise à jour vers la dernière version de Haveno. Une mise à jour obligatoire a été publiée, laquelle désactive le trading sur les anciennes versions. Veuillez consulter le Forum Haveno pour obtenir plus d'informations. popup.warning.noFilter=Nous n'avons pas reçu d'objet de filtre des nœuds de seed. Veuillez informer les administrateurs du réseau d'enregistrer un objet de filtre. -popup.warning.burnXMR=Cette transaction n''est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu''à ce que les frais de minage soient de nouveau bas ou jusqu''à ce que vous ayez accumulé plus de XMR à transférer. +popup.warning.burnXMR=Cette transaction n'est pas possible, car les frais de minage de {0} dépasseraient le montant à transférer de {1}. Veuillez patienter jusqu'à ce que les frais de minage soient de nouveau bas ou jusqu'à ce que vous ayez accumulé plus de XMR à transférer. -popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l''offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL''offre a été retirée pour éviter d''autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno disposible sur Keybase. +popup.warning.openOffer.makerFeeTxRejected=La transaction de frais de maker pour l'offre avec ID {0} a été rejetée par le réseau Monero.\nID de transaction={1}.\nL'offre a été retirée pour éviter d'autres problèmes.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno disposible sur Keybase. popup.warning.trade.txRejected.tradeFee=frais de transaction popup.warning.trade.txRejected.deposit=dépôt -popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Monero.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l'équipe Haveno est disponible sur Keybase. +popup.warning.trade.txRejected=La transaction {0} pour le trade qui a pour ID {1} a été rejetée par le réseau Monero.\nID de transaction={2}.\nLe trade a été déplacé vers les échanges échoués.\nAllez dans \"Paramètres/Info sur le réseau\" et effectuez une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. -popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l''offre avec ID {0} n''est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l''aide, le canal support de l''équipe Haveno est disponible sur Keybase. +popup.warning.openOfferWithInvalidMakerFeeTx=La transaction de frais de maker pour l'offre avec ID {0} n'est pas valide.\nID de transaction={1}.\nAllez dans \"Paramètres/Info sur le réseau réseau\" et faites une resynchronisation SPV.\nPour obtenir de l'aide, le canal support de l'équipe Haveno est disponible sur Keybase. popup.info.securityDepositInfo=Afin de s'assurer que les deux traders suivent le protocole de trading, les deux traders doivent payer un dépôt de garantie.\n\nCe dépôt est conservé dans votre portefeuille d'échange jusqu'à ce que votre transaction soit terminée avec succès, et ensuite il vous sera restitué.\n\nRemarque : si vous créez un nouvel ordre, Haveno doit être en cours d'exécution pour qu'un autre trader puisse l'accepter. Pour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste en ligne aussi (pour cela, assurez-vous qu'il ne passe pas en mode veille....le mode veille du moniteur ne pose aucun problème). -popup.info.cashDepositInfo=Veuillez vous assurer d''avoir une succursale de l''établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL''identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. +popup.info.cashDepositInfo=Veuillez vous assurer d'avoir une succursale de l'établissement bancaire dans votre région afin de pouvoir effectuer le dépôt en espèces.\nL'identifiant bancaire (BIC/SWIFT) de la banque du vendeur est: {0}. popup.info.cashDepositInfo.confirm=Je confirme que je peux effectuer le dépôt. popup.info.shutDownWithOpenOffers=Haveno est en cours de fermeture, mais des ordres sont en attente.\n\nCes ordres ne seront pas disponibles sur le réseau P2P si Haveno est éteint, mais ils seront republiés sur le réseau P2P la prochaine fois que vous lancerez Haveno.\n\nPour garder vos ordres en ligne, laissez Haveno en marche et assurez-vous que cet ordinateur reste aussi en ligne (pour cela, assurez-vous qu'il ne passe pas en mode veille...la veille du moniteur ne pose aucun problème). popup.info.qubesOSSetupInfo=Il semble que vous exécutez Haveno sous Qubes OS.\n\nVeuillez vous assurer que votre Haveno qube est mis en place de la manière expliquée dans notre guide [LIEN:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. @@ -1664,7 +1670,7 @@ popup.xmrLocalNode.msg=Haveno a détecté un nœud Monero en cours d'exécution popup.shutDownInProgress.headline=Fermeture en cours popup.shutDownInProgress.msg=La fermeture de l'application nécessite quelques secondes.\nVeuillez ne pas interrompre ce processus. -popup.attention.forTradeWithId=Attention requise la transaction avec l''ID {0} +popup.attention.forTradeWithId=Attention requise la transaction avec l'ID {0} popup.attention.reasonForPaymentRuleChange=La version 1.5.5 introduit un changement critique de règle de trade concernant le champ \"raison du paiement\" dans les transferts banquaires. Veuillez laisser ce champ vide -- N'UTILISEZ PAS l'ID de trade comme \"raison de paiement\". popup.info.multiplePaymentAccounts.headline=Comptes de paiement multiples disponibles @@ -1688,8 +1694,8 @@ popup.accountSigning.success.headline=Félicitations popup.accountSigning.success.description=Tous les {0} comptes de paiement ont été signés avec succès ! popup.accountSigning.generalInformation=Vous trouverez l'état de signature de tous vos comptes dans la section compte.\n\nPour plus d'informations, veuillez consulter [LIEN:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Un de vos comptes de paiement a été vérifié et signé par un arbitre. Echanger avec ce compte signera automatiquement le compte de votre pair de trading après un échange réussi.\n\n{0} -popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d''autres comptes dans les {0} jours à venir.\n\n{1} -popup.accountSigning.peerLimitLifted=La limite initiale pour l''un de vos comptes a été levée.\n\n{0} +popup.accountSigning.signedByPeer=Un de vos comptes de paiement a été vérifié et signé par un pair de trading. Votre limite de trading initiale sera levée et vous pourrez signer d'autres comptes dans les {0} jours à venir.\n\n{1} +popup.accountSigning.peerLimitLifted=La limite initiale pour l'un de vos comptes a été levée.\n\n{0} popup.accountSigning.peerSigner=Un de vos comptes est suffisamment mature pour signer d'autres comptes de paiement et la limite initiale pour un de vos comptes a été levée.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importer le témoin non-signé de l'âge du compte @@ -1712,8 +1718,8 @@ popup.info.buyerAsTakerWithoutDeposit=Votre offre ne nécessitera pas de dépôt # Notifications #################################################################### -notification.trade.headline=Notification pour la transaction avec l''ID {0} -notification.ticket.headline=Ticket de support pour l''échange avec l''ID {0} +notification.trade.headline=Notification pour la transaction avec l'ID {0} +notification.ticket.headline=Ticket de support pour l'échange avec l'ID {0} notification.trade.completed=La transaction est maintenant terminée et vous pouvez retirer vos fonds. notification.trade.accepted=Votre ordre a été accepté par un XMR {0}. notification.trade.unlocked=Votre échange avait au moins une confirmation sur la blockchain.\nVous pouvez effectuer le paiement maintenant. @@ -1723,7 +1729,7 @@ notification.trade.peerOpenedDispute=Votre pair de trading a ouvert un {0}. notification.trade.disputeClosed=Le {0} a été fermé notification.walletUpdate.headline=Mise à jour du portefeuille de trading notification.walletUpdate.msg=Votre portefeuille de trading est suffisamment approvisionné.\nMontant: {0} -notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d''une précédente tentative d''achat de l'ordre.\nMontant: {0} +notification.takeOffer.walletUpdate.msg=Votre portefeuille de trading était déjà suffisamment approvisionné à la suite d'une précédente tentative d'achat de l'ordre.\nMontant: {0} notification.tradeCompleted.headline=Le trade est terminé notification.tradeCompleted.msg=Vous pouvez retirer vos fonds vers un portefeuille Monero externe ou les conserver dans votre portefeuille Haveno. @@ -1736,25 +1742,25 @@ systemTray.show=Montrer la fenêtre de l'application systemTray.hide=Cacher la fenêtre de l'application systemTray.info=Informations au sujet de Haveno systemTray.exit=Sortir -systemTray.tooltip=Haveno: Une plateforme d''échange décentralisée sur le réseau monero +systemTray.tooltip=Haveno: Une plateforme d'échange décentralisée sur le réseau monero #################################################################### # GUI Util #################################################################### -guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l''arborescence:\n{0} +guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l'arborescence:\n{0} guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation. -guiUtil.accountExport.selectPath=Sélectionner l''arborescence vers {0} +guiUtil.accountExport.selectPath=Sélectionner l'arborescence vers {0} # suppress inspection "TrailingSpacesInProperty" -guiUtil.accountExport.tradingAccount=Compte de trading avec l''ID {0}\n +guiUtil.accountExport.tradingAccount=Compte de trading avec l'ID {0}\n # suppress inspection "TrailingSpacesInProperty" -guiUtil.accountImport.noImport=Nous n''avons pas importé de compte de trading avec l''id {0} car il existe déjà.\n -guiUtil.accountExport.exportFailed=Echec de l''export à CSV à cause d'une erreur.\nErreur = {0} +guiUtil.accountImport.noImport=Nous n'avons pas importé de compte de trading avec l'id {0} car il existe déjà.\n +guiUtil.accountExport.exportFailed=Echec de l'export à CSV à cause d'une erreur.\nErreur = {0} guiUtil.accountExport.selectExportPath=Sélectionner l'arborescence d'export -guiUtil.accountImport.imported=Compte de trading importé depuis l''arborescence:\n{0}\n\nComptes importés:\n{1} -guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n''a été trouvé sur l''arborescence {0}.\nLe nom du fichier est {1}." -guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n''utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" +guiUtil.accountImport.imported=Compte de trading importé depuis l'arborescence:\n{0}\n\nComptes importés:\n{1} +guiUtil.accountImport.noAccountsFound=Aucun compte de trading exporté n'a été trouvé sur l'arborescence {0}.\nLe nom du fichier est {1}." +guiUtil.openWebBrowser.warning=Vous allez ouvrir une page Web dans le navigateur Web de votre système.\nVoulez-vous ouvrir la page web maintenant ?\n\nSi vous n'utilisez pas le \"Navigateur Tor\" comme navigateur web par défaut, vous vous connecterez à la page web en clair.\n\nURL: \"{0}\" guiUtil.openWebBrowser.doOpen=Ouvrir la page web et ne plus me le demander guiUtil.openWebBrowser.copyUrl=Copier l'URL et annuler guiUtil.ofTradeAmount=du montant du trade @@ -1776,13 +1782,13 @@ table.placeholder.processingData=Traitement des données en cours... peerInfoIcon.tooltip.tradePeer=Du pair de trading peerInfoIcon.tooltip.maker=du maker peerInfoIcon.tooltip.trade.traded={0} adresse onion: {1}\nVous avez déjà échangé a {2} reprise(s) avec ce pair\n{3} -peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n''avez pas échangé avec ce pair jusqu''à présent.\n{2} +peerInfoIcon.tooltip.trade.notTraded={0} adresse onion: {1}\nvous n'avez pas échangé avec ce pair jusqu'à présent.\n{2} peerInfoIcon.tooltip.age=Compte de paiement créé il y a {0}. peerInfoIcon.tooltip.unknownAge=Ancienneté du compte de paiement inconnue. tooltip.openPopupForDetails=Ouvrir le popup pour obtenir des détails tooltip.invalidTradeState.warning=Le trade est dans un état invalide. Ouvrez la fenêtre des détails pour plus d'informations -tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l''adresse: {0} +tooltip.openBlockchainForAddress=Ouvrir un explorateur de blockchain externe pour l'adresse: {0} tooltip.openBlockchainForTx=Ouvrir un explorateur de blockchain externe pour la transaction: {0} confidence.unknown=Statut de transaction inconnu @@ -1966,7 +1972,7 @@ payment.accountNr=Numéro de compte payment.emailOrMobile=Email ou N° de portable payment.useCustomAccountName=Utiliser un nom de compte personnalisé payment.maxPeriod=Durée d'échange max. autorisée -payment.maxPeriodAndLimit=Durée maximale de l''échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} +payment.maxPeriodAndLimit=Durée maximale de l'échange : {0} / Achat maximum : {1} / Vente maximum : {2} / Âge du compte : {3} payment.maxPeriodAndLimitCrypto=Durée maximale de trade: {0} / Limite maximale de trading {1} payment.currencyWithSymbol=Devise: {0} payment.nameOfAcceptedBank=Nom de la banque acceptée @@ -1993,7 +1999,7 @@ payment.halCash.info=Lors de l'utilisation de HalCash, l'acheteur de XMR doit en # suppress inspection "UnusedMessageFormatParameter" payment.limits.info=Sachez que tous les virements bancaires comportent un certain risque de rétrofacturation. Pour mitiger ce risque, Haveno fixe des limites par trade en fonction du niveau estimé de risque de rétrofacturation pour la méthode de paiement utilisée.\n\nPour cette méthode de paiement, votre limite de trading pour l'achat et la vente est de {2}.\n\nCette limite ne s'applique qu'à la taille d'une seule transaction. Vous pouvez effectuer autant de transactions que vous le souhaitez.\n\nVous trouverez plus de détails sur le wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d''nformations, rendez vous à [LIEN:https://docs.haveno.exchange/the-project/account_limits]. +payment.limits.info.withSigning=Afin de limiter le risque de rétrofacturation des achats, Haveno fixe des limites d'achat par transaction pour ce compte de paiement basé sur les 2 facteurs suivants :\n\n1. Risque de rétrofacturation pour le mode de paiement\n2. Statut de signature du compte\n\nCe compte de paiement n'est pas encore signé, il est donc limité à l'achat de {0} par trade. Après sa signature, les limites d'achat augmenteront comme suit :\n\n● Avant la signature, et jusqu'à 30 jours après la signature, votre limite d'achat par trade sera de {0}\n● 30 jours après la signature, votre limite d'achat par trade sera de {1}\n● 60 jours après la signature, votre limite d'achat par trade sera de {2}\n\nLes limites de vente ne sont pas affectées par la signature du compte. Vous pouvez vendre {2} en un seul trade immédiatement.\n\nCes limites s'appliquent uniquement à la taille d'un seul trade-vous pouvez placer autant de trades que vous voulez.\n\n Pour plus d'nformations, rendez vous à [LIEN:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Veuillez confirmer que votre banque vous permet d'envoyer des dépôts en espèces sur le compte d'autres personnes. Par exemple, Bank of America et Wells Fargo n'autorisent plus de tels dépôts. @@ -2216,8 +2222,8 @@ validation.negative=Une valeur négative n'est pas autorisée. validation.traditional.tooSmall=La saisie d'une valeur plus petite que le montant minimal possible n'est pas autorisée. validation.traditional.tooLarge=La saisie d'une valeur supérieure au montant maximal possible n'est pas autorisée. validation.xmr.fraction=L'entrée résultera dans une valeur monero plus petite qu'1 satoshi -validation.xmr.tooLarge=La saisie d''une valeur supérieure à {0} n''est pas autorisée. -validation.xmr.tooSmall=La saisie d''une valeur inférieure à {0} n''est pas autorisée. +validation.xmr.tooLarge=La saisie d'une valeur supérieure à {0} n'est pas autorisée. +validation.xmr.tooSmall=La saisie d'une valeur inférieure à {0} n'est pas autorisée. validation.passwordTooShort=Le mot de passe que vous avez saisi est trop court. Il doit comporter un minimum de 8 caractères. validation.passwordTooLong=Le mot de passe que vous avez saisi est trop long. Il ne doit pas contenir plus de 50 caractères. validation.sortCodeNumber={0} doit être composer de {1} chiffres. @@ -2225,17 +2231,17 @@ validation.sortCodeChars={0} doit être composer de {1} caractères. validation.bankIdNumber={0} doit être composer de {1} chiffres. validation.accountNr=Le numéro du compte doit comporter {0} chiffres. validation.accountNrChars=Le numéro du compte doit comporter {0} caractères. -validation.xmr.invalidAddress=L''adresse n''est pas correcte. Veuillez vérifier le format de l''adresse. +validation.xmr.invalidAddress=L'adresse n'est pas correcte. Veuillez vérifier le format de l'adresse. validation.integerOnly=Veuillez seulement entrer des nombres entiers. validation.inputError=Votre saisie a causé une erreur:\n{0} -validation.xmr.exceedsMaxTradeLimit=Votre seuil maximum d''échange est {0}. +validation.xmr.exceedsMaxTradeLimit=Votre seuil maximum d'échange est {0}. validation.nationalAccountId={0} doit être composé de {1} nombres. #new validation.invalidInput=La valeur saisie est invalide: {0} validation.accountNrFormat=Le numéro du compte doit être au format: {0} # suppress inspection "UnusedProperty" -validation.crypto.wrongStructure=La validation de l''adresse a échoué car elle ne concorde pas avec la structure d''une adresse {0}. +validation.crypto.wrongStructure=La validation de l'adresse a échoué car elle ne concorde pas avec la structure d'une adresse {0}. # suppress inspection "UnusedProperty" validation.crypto.ltz.zAddressesNotSupported=L'adresse LTZ doit commencer par L. Les adresses commençant par z ne sont pas supportées. # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 4a5d177c0a..d00857a2db 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Sotto % del prezzo di mercato shared.aboveInPercent=Sopra % del prezzo di mercato shared.enterPercentageValue=Immetti il valore % shared.OR=OPPURE -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=In attesa dei fondi... shared.TheXMRBuyer=L'acquirente di XMR shared.You=Tu @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=Al momento, hai troppe transazioni non confermate. Per favore riprova più tardi. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=Revisione: piazza l'offerta a {0} monero +createOffer.placeOfferButton.buy=Revisiona: Crea offerta per acquistare XMR con {0} +createOffer.placeOfferButton.sell=Revisiona: Crea offerta per vendere XMR in cambio di {0} createOffer.createOfferFundWalletInfo.headline=Finanzia la tua offerta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n @@ -524,7 +520,8 @@ takeOffer.success.info=Puoi vedere lo stato del tuo scambio su \"Portafoglio/Sca takeOffer.error.message=Si è verificato un errore durante l'accettazione dell'offerta.\n\n{0} # new entries -takeOffer.takeOfferButton=Rivedi: Accetta l'offerta per {0} monero +takeOffer.takeOfferButton.buy=Revisiona: Accetta offerta per acquistare XMR con {0} +takeOffer.takeOfferButton.sell=Revisiona: Accetta offerta per vendere XMR in cambio di {0} takeOffer.noPriceFeedAvailable=Non puoi accettare questa offerta poiché utilizza un prezzo in percentuale basato sul prezzo di mercato ma non è disponibile alcun feed di prezzi. takeOffer.takeOfferFundWalletInfo.headline=Finanzia il tuo scambio # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Attendi la conferma della blockchain +portfolio.pending.step2_buyer.additionalConf=I depositi hanno raggiunto 10 conferme.\nPer maggiore sicurezza, consigliamo di attendere {0} conferme prima di inviare il pagamento.\nProcedi in anticipo a tuo rischio. portfolio.pending.step2_buyer.startPayment=Inizia il pagamento portfolio.pending.step2_seller.waitPaymentSent=Attendi fino all'avvio del pagamento portfolio.pending.step3_buyer.waitPaymentArrived=Attendi fino all'arrivo del pagamento @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunit # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Contatta il venditore XMR tramite il contatto fornito e organizza un incontro per pagare {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Inizia il pagamento utilizzando {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Informazioni di contatto dell'ac portfolio.pending.step2_seller.waitPayment.msg=La transazione di deposito necessita di almeno una conferma blockchain.\nDevi attendere fino a quando l'acquirente XMR invia il pagamento {0}. portfolio.pending.step2_seller.warn=L'acquirente XMR non ha ancora effettuato il pagamento {0}.\nDevi aspettare fino a quando non invia il pagamento.\nSe lo scambio non sarà completato il {1}, l'arbitro comincierà ad indagare. portfolio.pending.step2_seller.openForDispute=L'acquirente XMR non ha ancora inviato il pagamento!\nIl periodo massimo consentito per lo scambio è trascorso.\nPuoi aspettare più a lungo e dare più tempo al partner di scambio oppure puoi contattare il mediatore per ricevere assistenza. -tradeChat.chatWindowTitle=Finestra di chat per scambi con ID '' {0} '' +tradeChat.chatWindowTitle=Finestra di chat per scambi con ID ' {0} ' tradeChat.openChat=Apri la finestra di chat tradeChat.rules=Puoi comunicare con il tuo peer di trading per risolvere potenziali problemi con questo scambio.\nNon è obbligatorio rispondere nella chat.\nSe un trader viola una delle seguenti regole, apri una controversia ed effettua una segnalazione al mediatore o all'arbitro.\n\nRegole della chat:\n● Non inviare nessun link (rischio di malware). È possibile inviare l'ID transazione e il nome di un block explorer.\n● Non inviare parole del seed, chiavi private, password o altre informazioni sensibili!\n● Non incoraggiare il trading al di fuori di Haveno (non garantisce nessuna sicurezza).\n● Non intraprendere alcuna forma di tentativo di frode di ingegneria sociale.\n● Se un peer non risponde e preferisce non comunicare tramite chat, rispettane la decisione.\n● Limita l'ambito della conversazione allo scambio. Questa chat non è una sostituzione di messenger o un troll-box.\n● Mantieni la conversazione amichevole e rispettosa.\n  @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=In caso di problemi, puoi provare a conta portfolio.pending.support.button.getHelp=Apri la chat dello scambio portfolio.pending.support.headline.halfPeriodOver=Controlla il pagamento portfolio.pending.support.headline.periodOver=Il periodo di scambio è finito +portfolio.pending.support.headline.depositTxMissing=Transazione di deposito mancante +portfolio.pending.support.depositTxMissing=Manca una transazione di deposito per questa operazione. Apri un ticket di supporto per contattare un arbitro per assistenza. portfolio.pending.mediationRequested=Mediazione richiesta portfolio.pending.refundRequested=Rimborso richiesto @@ -812,14 +812,14 @@ portfolio.pending.mediationResult.info.peerAccepted=Il tuo pari commerciale ha a portfolio.pending.mediationResult.button=Visualizza la risoluzione proposta portfolio.pending.mediationResult.popup.headline=Risultato della mediazione per gli scambi con ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Il tuo pari commerciale ha accettato il suggerimento del mediatore per lo scambio {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rifiuta e richiedi l'arbitrato portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Manca una transazione di deposito.\n\nQuesta transazione è necessaria per completare lo scambio. Assicurati che il tuo portafoglio sia completamente sincronizzato con la blockchain di Monero.\n\nPuoi spostare questo scambio nella sezione "Scambi Falliti" per disattivarlo. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -927,8 +927,6 @@ support.tab.mediation.support=Mediazione support.tab.arbitration.support=Arbitrato support.tab.legacyArbitration.support=Arbitrato Legacy support.tab.ArbitratorsSupportTickets=I ticket di {0} -support.filter=Search disputes -support.filter.prompt=Inserisci ID commerciale, data, indirizzo onion o dati dell'account support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -989,7 +987,7 @@ support.youOpenedDisputeForMediation=Hai richiesto la mediazione.\n\n{0}\n\nVers support.peerOpenedTicket=Il tuo peer di trading ha richiesto supporto a causa di problemi tecnici.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDispute=Il tuo peer di trading ha richiesto una controversia.\n\n{0}\n\nVersione Haveno: {1} support.peerOpenedDisputeForMediation=Il tuo peer di trading ha richiesto la mediazione.\n\n{0}\n\nVersione Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Indirizzo nodo del mediatore: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1034,6 +1032,7 @@ setting.preferences.displayOptions=Mostra opzioni setting.preferences.showOwnOffers=Mostra le mie offerte nel libro delle offerte setting.preferences.useAnimations=Usa animazioni setting.preferences.useDarkMode=Usa modalità notte +setting.preferences.useLightMode=Usa la modalità chiara setting.preferences.sortWithNumOffers=Ordina le liste di mercato con n. di offerte/scambi setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1049,6 +1048,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nuova funzione per la privacy dei dati +settings.preferences.sensitiveDataRemoval.msg=Per proteggere la privacy tua e degli altri trader, Haveno intende rimuovere i dati sensibili dalle vecchie transazioni. Questo è particolarmente importante per le transazioni in valuta fiat che possono includere dettagli del conto bancario.\n\nSi consiglia di impostare questo valore il più basso possibile, ad esempio 60 giorni. Questo significa che le transazioni completate da più di 60 giorni avranno i dati sensibili cancellati. Le transazioni completate si trovano nella scheda Portafoglio / Cronologia. + settings.net.xmrHeader=Network Monero settings.net.p2pHeader=Rete Haveno settings.net.onionAddressLabel=Il mio indirizzo onion @@ -1115,19 +1117,19 @@ setting.about.subsystems.label=Versioni di sottosistemi setting.about.subsystems.val=Versione di rete: {0}; Versione del messaggio P2P: {1}; Versione DB locale: {2}; Versione del protocollo di scambio: {3} setting.about.shortcuts=Scorciatoie -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' o ''alt + {0}'' o ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' o 'alt + {0}' o 'cmd + {0}' setting.about.shortcuts.menuNav=Naviga il menu principale setting.about.shortcuts.menuNav.value=Per navigare nel menu principale premere: 'Ctrl' o 'alt' o 'cmd' con un tasto numerico tra '1-9' setting.about.shortcuts.close=Chiudi Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' o ''cmd + {0}'' o ''Ctrl + {1}'' o ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' o 'cmd + {0}' o 'Ctrl + {1}' o 'cmd + {1}' setting.about.shortcuts.closePopup=Chiudi popup o finestra di dialogo setting.about.shortcuts.closePopup.value=Tasto 'ESC' setting.about.shortcuts.chatSendMsg=Invia messaggio chat al trader -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' o ''alt + ENTER'' o ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' o 'alt + ENTER' o 'cmd + ENTER' setting.about.shortcuts.openDispute=Apri disputa setting.about.shortcuts.openDispute.value=Seleziona lo scambio in sospeso e fai clic: {0} @@ -1318,7 +1320,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo di offerta account.notifications.marketAlert.message.title=Avviso di offerta account.notifications.marketAlert.message.msg.below=sotto account.notifications.marketAlert.message.msg.above=sopra -account.notifications.marketAlert.message.msg=Una nuova ''{0} {1}'' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento ''{5}'' è stata pubblicata sulla pagina delle offerte Haveno.\nID offerta: {6}. +account.notifications.marketAlert.message.msg=Una nuova '{0} {1}' offerta con prezzo {2} ({3} {4} prezzo di mercato) e metodo di pagamento '{5}' è stata pubblicata sulla pagina delle offerte Haveno.\nID offerta: {6}. account.notifications.priceAlert.message.title=Avviso di prezzo per {0} account.notifications.priceAlert.message.msg=Il tuo avviso di prezzo è stato attivato. L'attuale prezzo {0} è {1} {2} account.notifications.noWebCamFound.warning=Nessuna webcam trovata.\n\nUtilizzare l'opzione e-mail per inviare il token e la chiave di crittografia dal telefono cellulare all'applicazione Haveno. @@ -1471,11 +1473,14 @@ offerDetailsWindow.countryBank=Paese della banca del maker offerDetailsWindow.commitment=Impegno offerDetailsWindow.agree=Accetto offerDetailsWindow.tac=Termini e condizioni -offerDetailsWindow.confirm.maker=Conferma: Piazza l'offerta a {0} monero -offerDetailsWindow.confirm.taker=Conferma: Accetta l'offerta a {0} monero +offerDetailsWindow.confirm.maker.buy=Conferma: Crea offerta per acquistare XMR con {0} +offerDetailsWindow.confirm.maker.sell=Conferma: Crea offerta per vendere XMR in cambio di {0} +offerDetailsWindow.confirm.taker.buy=Conferma: Accetta offerta per acquistare XMR con {0} +offerDetailsWindow.confirm.taker.sell=Conferma: Accetta offerta per vendere XMR in cambio di {0} offerDetailsWindow.creationDate=Data di creazione offerDetailsWindow.makersOnion=Indirizzo .onion del maker offerDetailsWindow.challenge=Passphrase dell'offerta +offerDetailsWindow.challenge.copy=Copia la frase segreta da condividere con il tuo interlocutore qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1561,7 +1566,7 @@ torNetworkSettingWindow.deleteFiles.button=Rimuovi i file Tor obsoleti e spegni torNetworkSettingWindow.deleteFiles.progress=Spegnimento Tor in corso torNetworkSettingWindow.deleteFiles.success=File obsoleti di Tor eliminati con successo. Riavvia. torNetworkSettingWindow.bridges.header=Tor è bloccato? -torNetworkSettingWindow.bridges.info=Se Tor è bloccato dal tuo provider di servizi Internet o dal tuo paese, puoi provare a utilizzare i bridge Tor.\nVisitare la pagina Web Tor all'indirizzo: https://bridges.torproject.org/bridges per ulteriori informazioni sui bridge e sui trasporti collegabili.\n  +torNetworkSettingWindow.bridges.info=Se Tor è bloccato dal tuo provider di servizi Internet o dal tuo paese, puoi provare a utilizzare i bridge Tor.\nVisitare la pagina Web Tor all'indirizzo: https://bridges.torproject.org per ulteriori informazioni sui bridge e sui trasporti collegabili.\n  feeOptionWindow.headline=Scegli la valuta per il pagamento delle commissioni commerciali feeOptionWindow.info=Puoi scegliere di pagare la commissione commerciale in BSQ o in XMR. Se scegli BSQ approfitti della commissione commerciale scontata. @@ -1621,6 +1626,7 @@ popup.warning.noPriceFeedAvailable=Non è disponibile alcun feed di prezzi per l popup.warning.sendMsgFailed=Invio del messaggio al tuo partner commerciale non riuscito.\nTi preghiamo di riprovare e se continua a fallire segnalare un bug. popup.warning.messageTooLong=Il tuo messaggio supera la dimensione massima consentita. Si prega di inviarlo in più parti o caricarlo su un servizio come https://pastebin.com. popup.warning.lockedUpFunds=Hai bloccato i fondi da uno scambio fallito.\nSaldo bloccato: {0}\nIndirizzo tx deposito: {1}\nID scambio: {2}.\n\nApri un ticket di supporto selezionando lo scambio nella schermata degli scambi aperti e premendo \"alt + o\" o \"option + o\"." +popup.warning.moneroConnection=Si è verificato un problema durante la connessione alla rete Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1978,7 +1984,7 @@ payment.accountType=Tipologia conto payment.checking=Verifica payment.savings=Risparmi payment.personalId=ID personale -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. payment.fasterPayments.newRequirements.info=Alcune banche hanno iniziato a verificare il nome completo del destinatario per i trasferimenti di Faster Payments (UK). Il tuo attuale account Faster Payments non specifica un nome completo.\n\nTi consigliamo di ricreare il tuo account Faster Payments in Haveno per fornire ai futuri acquirenti {0} un nome completo.\n\nQuando si ricrea l'account, assicurarsi di copiare il codice di ordinamento preciso, il numero di account e i valori salt della verifica dell'età dal vecchio account al nuovo account. Ciò garantirà il mantenimento dell'età del tuo account esistente e lo stato della firma.\n  payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. @@ -1991,7 +1997,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Conferma che la tua banca ti consente di inviare depositi in contanti su conti di altre persone. Ad esempio, Bank of America e Wells Fargo non consentono più tali depositi. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Si prega di notare che Cash App ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. @@ -2025,7 +2031,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Per la tua protezione, sconsigliamo vivamente di utilizzare i PIN di Paysafecard per i pagamenti.\n\n\ Le transazioni effettuate tramite PIN non possono essere verificate in modo indipendente per la risoluzione delle controversie. Se si verifica un problema, il recupero dei fondi potrebbe non essere possibile.\n\n\ Per garantire la sicurezza delle transazioni con risoluzione delle controversie, utilizza sempre metodi di pagamento che forniscono registrazioni verificabili. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 3514d37343..ee23831447 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=価値は{0}より高くなければな createOffer.triggerPrice.invalid.tooHigh=価値は{0}より低くなければなりません # new entries -createOffer.placeOfferButton=再確認: ビットコインを{0}オファーを出す +createOffer.placeOfferButton.buy=確認:{0}でXMRを購入するオファーを作成 +createOffer.placeOfferButton.sell=確認:{0}でXMRを売却するオファーを作成 createOffer.createOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 取引額: {0}\n @@ -525,7 +521,8 @@ takeOffer.success.info=あなたのトレード状態は「ポートフォリオ takeOffer.error.message=オファーの受け入れ時にエラーが発生しました。\n\n{0} # new entries -takeOffer.takeOfferButton=再確認: ビットコインを{0}オファーを申し込む +takeOffer.takeOfferButton.buy=確認:{0}でXMRを購入するオファーを受け入れ +takeOffer.takeOfferButton.sell=確認:{0}でXMRを売却するオファーを受け入れ takeOffer.noPriceFeedAvailable=そのオファーは市場価格に基づくパーセント値を使用していますが、使用可能な価格フィードがないため、利用することはできません。 takeOffer.takeOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=市場からの割合価格偏差 portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=ブロックチェーンの承認をお待ち下さい +portfolio.pending.step2_buyer.additionalConf=入金は10承認に達しました。\n追加の安全のため、支払いを送信する前に{0}承認を待つことをお勧めします。\n早めに進める場合は自己責任となります。 portfolio.pending.step2_buyer.startPayment=支払い開始 portfolio.pending.step2_seller.waitPaymentSent=支払いが始まるまでお待ち下さい portfolio.pending.step3_buyer.waitPaymentArrived=支払いが到着するまでお待ち下さい @@ -676,7 +674,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=買い手の連絡先 portfolio.pending.step2_seller.waitPayment.msg=デポジットトランザクションには、少なくとも1つのブロックチェーン承認があります。\nXMRの買い手が{0}の支払いを開始するまで待つ必要があります。 portfolio.pending.step2_seller.warn=XMRの買い手はまだ{0}の支払いを行っていません。\n支払いが開始されるまで待つ必要があります。\n取引が{1}で完了していない場合は、調停人が調査します。 portfolio.pending.step2_seller.openForDispute=XMRの買い手は支払いを開始していません!\nトレードの許可された最大期間が経過しました。\nもっと長く待ってトレードピアにもっと時間を与えるか、助けを求めるために調停者に連絡することができます。 -tradeChat.chatWindowTitle=トレードID '{0}'' のチャットウィンドウ +tradeChat.chatWindowTitle=トレードID '{0}' のチャットウィンドウ tradeChat.openChat=チャットウィンドウを開く tradeChat.rules=このトレードに対する潜在的な問題を解決するため、トレードピアと連絡できます。\nチャットに返事する義務はありません。\n取引者が以下のルールを破ると、係争を開始して調停者や調停人に報告して下さい。\n\nチャット・ルール:\n\t●リンクを送らないこと(マルウェアの危険性)。トランザクションIDとブロックチェーンエクスプローラの名前を送ることができます。\n\t●シードワード、プライベートキー、パスワードなどの機密な情報を送らないこと。\n\t●Haveno外のトレードを助長しないこと(セキュリティーがありません)。\n\t●ソーシャル・エンジニアリングや詐欺の行為に参加しないこと。\n\t●チャットで返事されない場合、それともチャットでの連絡が断られる場合、ピアの決断を尊重すること。\n\t●チャットの範囲をトレードに集中しておくこと。チャットはメッセンジャーの代わりや釣りをする場所ではありません。\n\t●礼儀正しく丁寧に話すこと。 @@ -795,6 +793,8 @@ portfolio.pending.support.text.getHelp=問題があれば、トレードチャ portfolio.pending.support.button.getHelp=取引者チャットを開く portfolio.pending.support.headline.halfPeriodOver=支払いを確認 portfolio.pending.support.headline.periodOver=トレード期間は終了しました +portfolio.pending.support.headline.depositTxMissing=入金トランザクションが見つかりません +portfolio.pending.support.depositTxMissing=この取引には入金トランザクションが見つかりません。サポートチケットを開いて、仲裁者に連絡してサポートを受けてください。 portfolio.pending.mediationRequested=調停は依頼されました portfolio.pending.refundRequested=返金は請求されました @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=すでに受け入れて portfolio.pending.failedTrade.taker.missingTakerFeeTx=欠測テイカー手数料のトランザクション。\n\nこのtxがなければ、トレードを完了できません。資金はロックされず、トレード手数料は支払いませんでした。「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=ピアのテイカー手数料のトランザクションは欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでした。あなたのオファーがまだ他の取引者には有効ですので、メイカー手数料は失っていません。このトレードを「失敗トレード」へ送ることができます。 -portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nこのトレードを「失敗トレード」へ送れます。 +portfolio.pending.failedTrade.missingDepositTx=入金トランザクションが見つかりません。\n\nこのトランザクションは取引を完了するために必要です。Moneroブロックチェーンとウォレットが完全に同期されていることを確認してください。\n\nこの取引を「失敗した取引」セクションに移動して、無効化することができます。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -927,8 +927,6 @@ support.tab.mediation.support=調停 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=レガシー仲裁 support.tab.ArbitratorsSupportTickets={0} のチケット -support.filter=係争を検索 -support.filter.prompt=トレードID、日付、onionアドレスまたはアカウントデータを入力してください support.sigCheck.button=Check signature support.sigCheck.popup.info=仲裁プロセスの要約メッセージを貼り付けてください。このツールを使用すると、どんなユーザーでも仲裁者の署名が要約メッセージと一致するかどうかを確認できます。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=表示設定 setting.preferences.showOwnOffers=オファーブックに自分のオファーを表示 setting.preferences.useAnimations=アニメーションを使用 setting.preferences.useDarkMode=ダークモードを利用 +setting.preferences.useLightMode=ライトモードを使用する setting.preferences.sortWithNumOffers=市場リストをオファー/トレードの数で並び替える setting.preferences.onlyShowPaymentMethodsFromAccount=サポートされていない支払い方法を非表示にする setting.preferences.denyApiTaker=APIを使用するテイカーを拒否する @@ -1050,6 +1049,9 @@ settings.preferences.editCustomExplorer.name=名義 settings.preferences.editCustomExplorer.txUrl=トランザクションURL settings.preferences.editCustomExplorer.addressUrl=アドレスURL +setting.info.headline=新しいデータプライバシー機能 +settings.preferences.sensitiveDataRemoval.msg=ご自身および他のトレーダーのプライバシーを保護するため、Havenoは過去の取引から機密データを削除する予定です。これは銀行口座情報を含む可能性のある法定通貨の取引で特に重要です。\n\n可能な限り低く設定することをお勧めします。例えば60日です。これは、60日以上前の完了した取引の機密データが削除されることを意味します。完了した取引はポートフォリオ/履歴タブで確認できます。 + settings.net.xmrHeader=ビットコインのネットワーク settings.net.p2pHeader=Havenoネットワーク settings.net.onionAddressLabel=私のonionアドレス @@ -1472,11 +1474,14 @@ offerDetailsWindow.countryBank=メイカーの銀行の国名 offerDetailsWindow.commitment=約束 offerDetailsWindow.agree=同意します offerDetailsWindow.tac=取引条件 -offerDetailsWindow.confirm.maker=承認: ビットコインを{0}オファーを出す -offerDetailsWindow.confirm.taker=承認: ビットコインを{0}オファーを受ける +offerDetailsWindow.confirm.maker.buy=確定:{0}でXMRを購入するオファーを作成 +offerDetailsWindow.confirm.maker.sell=確定:{0}でXMRを売却するオファーを作成 +offerDetailsWindow.confirm.taker.buy=確定:{0}でXMRを購入するオファーを受け入れ +offerDetailsWindow.confirm.taker.sell=確定:{0}でXMRを売却するオファーを受け入れ offerDetailsWindow.creationDate=作成日 offerDetailsWindow.makersOnion=メイカーのonionアドレス offerDetailsWindow.challenge=オファーパスフレーズ +offerDetailsWindow.challenge.copy=ピアと共有するためにパスフレーズをコピーする qRCodeWindow.headline=QRコード qRCodeWindow.msg=外部ウォレットからHavenoウォレットへ送金するのに、このQRコードを利用して下さい。 @@ -1562,7 +1567,7 @@ torNetworkSettingWindow.deleteFiles.button=Torの古いファイルを削除し torNetworkSettingWindow.deleteFiles.progress=Torをシャットダウン中 torNetworkSettingWindow.deleteFiles.success=Torの古いファイルの削除に成功しました。再起動してください。 torNetworkSettingWindow.bridges.header=Torはブロックされていますか? -torNetworkSettingWindow.bridges.info=Torがあなたのインターネットプロバイダや国にブロックされている場合、Torブリッジによる接続を試みることができます。\nTorのWebページ https://bridges.torproject.org/bridges にアクセスして、ブリッジとプラガブル転送について学べます +torNetworkSettingWindow.bridges.info=Torがあなたのインターネットプロバイダや国にブロックされている場合、Torブリッジによる接続を試みることができます。\nTorのWebページ https://bridges.torproject.org にアクセスして、ブリッジとプラガブル転送について学べます feeOptionWindow.headline=取引手数料の支払いに使用する通貨を選択してください feeOptionWindow.info=あなたは取引手数料の支払いにBSQまたはXMRを選択できます。 BSQを選択した場合は、割引された取引手数料に気付くでしょう。 @@ -1622,6 +1627,7 @@ popup.warning.noPriceFeedAvailable=その通貨で利用できる価格フィー popup.warning.sendMsgFailed=トレード相手へのメッセージの送信に失敗しました。\nもう一度試してください。失敗し続ける場合はバグを報告してください。 popup.warning.messageTooLong=メッセージが許容サイズ上限を超えています。いくつかに分けて送信するか、 https://pastebin.com のようなサービスにアップロードしてください。 popup.warning.lockedUpFunds=失敗したトレードから残高をロックしました。\nロックされた残高: {0} \nデポジットtxアドレス: {1} \nトレードID: {2}。\n\nオープントレード画面でこのトレードを選択し、「alt + o」または「option + o」を押してサポートチケットを開いてください。 +popup.warning.moneroConnection=Moneroネットワークへの接続中に問題が発生しました。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 03d90cc9fd..0e92411654 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=% abaixo do preço de mercado shared.aboveInPercent=% acima do preço de mercado shared.enterPercentageValue=Insira a % shared.OR=OU -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Aguardando pagamento... shared.TheXMRBuyer=O comprador de XMR shared.You=Você @@ -221,7 +216,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=No momento, você possui muitas transações não-confirmadas. Tente novamente mais tarde. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -462,7 +457,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=Revisar: Criar oferta para {0} monero +createOffer.placeOfferButton.buy=Revisar: Criar oferta para comprar XMR com {0} +createOffer.placeOfferButton.sell=Revisar: Criar oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia da negociação: {0} \n @@ -527,7 +523,8 @@ takeOffer.success.info=Você pode ver o status de sua negociação em \"Portfoli takeOffer.error.message=Ocorreu um erro ao aceitar a oferta.\n\n{0} # new entries -takeOffer.takeOfferButton=Revisar: Aceitar oferta para {0} monero +takeOffer.takeOfferButton.buy=Revisar: Aceitar oferta para comprar XMR com {0} +takeOffer.takeOfferButton.sell=Revisar: Aceitar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=Você não pode aceitar essa oferta pois ela usa uma porcentagem do preço baseada no preço de mercado, mas o canal de preços está indisponível no momento. takeOffer.takeOfferFundWalletInfo.headline=Financiar sua negociação # suppress inspection "TrailingSpacesInProperty" @@ -582,6 +579,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Aguardar confirmação da blockchain +portfolio.pending.step2_buyer.additionalConf=Depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProssiga antecipadamente por sua própria conta e risco. portfolio.pending.step2_buyer.startPayment=Iniciar pagamento portfolio.pending.step2_seller.waitPaymentSent=Aguardar início do pagamento portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar recebimento do pagamento @@ -646,7 +644,7 @@ portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Ord # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor, entre em contato com o vendedor de XMR através do contato fornecido e combine um encontro para pagá-lo {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} @@ -797,6 +795,8 @@ portfolio.pending.support.text.getHelp=Caso tenha problemas, você pode tentar c portfolio.pending.support.button.getHelp=Abrir Chat de Negociante portfolio.pending.support.headline.halfPeriodOver=Verifique o pagamento portfolio.pending.support.headline.periodOver=O período de negociação acabou +portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente +portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência. portfolio.pending.mediationRequested=Mediação requerida portfolio.pending.refundRequested=Reembolso requerido @@ -815,14 +815,14 @@ portfolio.pending.mediationResult.info.peerAccepted=O seu parceiro de negociaç portfolio.pending.mediationResult.button=Ver solução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para a negociação com ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu parceiro de negociação aceitou a sugestão do mediador para a negociação {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitramento portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Por favor, certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações Falhas" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -929,8 +929,6 @@ support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitração antiga support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Search disputes -support.filter.prompt=Insira ID da negociação. data. endereço onion ou dados da conta support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -991,7 +989,7 @@ support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVers support.peerOpenedTicket=O seu parceiro de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedDispute=O seu parceiro de negociação solicitou uma disputa.\n\n{0}\n\nVersão do Haveno: {1} support.peerOpenedDisputeForMediation=O seu parceiro de negociação solicitou mediação.\n\n{0}\n\nVersão do Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1036,6 +1034,7 @@ setting.preferences.displayOptions=Opções de exibição setting.preferences.showOwnOffers=Exibir minhas ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar modo escuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar pelo nº de ofertas/negociações setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1051,6 +1050,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nova funcionalidade de privacidade de dados +settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros traders, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nÉ recomendado definir o menor valor possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. + settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede Haveno settings.net.onionAddressLabel=Meu endereço onion @@ -1117,19 +1119,19 @@ setting.about.subsystems.label=Versões dos subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagens P2P: {1}; Versão do banco de dados local: {2}; Versão do protocolo de negociação: {3} setting.about.shortcuts=atalhos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navegar para o menu principal setting.about.shortcuts.menuNav.value=Para ir ao menu principal, pressione: "ctr" ou "alt" ou "cmd" com um botão numérico de 1 a 9 setting.about.shortcuts.close=Fechar Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=botão "Esc" setting.about.shortcuts.chatSendMsg=Enviar mensagem de chat ao negociador -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecione negociação pendente e clique: {0} @@ -1320,7 +1322,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo account.notifications.marketAlert.message.msg.above=acima -account.notifications.marketAlert.message.msg=Uma nova oferta ''{0} {1}'' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento ''{5}'' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. +account.notifications.marketAlert.message.msg=Uma nova oferta '{0} {1}' com preço {2} ({3} {4} preço de mercado) e com o método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O seu preço de alerta foi atingido. O preço atual da {0} é {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor, use a opção e-mail para enviar o token e a chave de criptografia do seu celular para o Haveno @@ -1475,11 +1477,14 @@ offerDetailsWindow.countryBank=País do banco do ofertante offerDetailsWindow.commitment=Compromisso offerDetailsWindow.agree=Eu concordo offerDetailsWindow.tac=Termos e condições -offerDetailsWindow.confirm.maker=Criar oferta para {0} monero -offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero +offerDetailsWindow.confirm.maker.buy=Confirmar: Criar oferta para comprar XMR com {0} +offerDetailsWindow.confirm.maker.sell=Confirmar: Criar oferta para vender XMR por {0} +offerDetailsWindow.confirm.taker.buy=Confirmar: Aceitar oferta para comprar XMR com {0} +offerDetailsWindow.confirm.taker.sell=Confirmar: Aceitar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Criada em offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta +offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu par qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1566,7 +1571,7 @@ torNetworkSettingWindow.deleteFiles.button=Apagar arquivos desatualizados do Tor torNetworkSettingWindow.deleteFiles.progress=Desligando Tor... torNetworkSettingWindow.deleteFiles.success=Os arquivos desatualizados do Tor foram deletados com sucesso. Por favor, reinicie o aplicativo. torNetworkSettingWindow.bridges.header=O Tor está bloqueado? -torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu provedor de internet ou em seu país, você pode tentar usar pontes do Tor.\nVisite a página do Tor em https://bridges.torproject.org/bridges para aprender mais sobre pontes e transportadores plugáveis. +torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu provedor de internet ou em seu país, você pode tentar usar pontes do Tor.\nVisite a página do Tor em https://bridges.torproject.org para aprender mais sobre pontes e transportadores plugáveis. feeOptionWindow.headline=Escolha a moeda para pagar a taxa de negociação feeOptionWindow.info=Você pode optar por pagar a taxa de negociação em BSQ ou XMR. As taxas de negociação são reduzidas quando pagas com BSQ. @@ -1628,6 +1633,7 @@ popup.warning.btcChangeBelowDustException=Esta transação cria um troco menor d popup.warning.messageTooLong=Sua mensagem excede o tamanho máximo permitido. Favor enviá-la em várias partes ou utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você possui fundos travados em uma negociação com erro.\nSaldo travado: {0}\nEndereço da transação de depósito: {1}\nID da negociação: {2}.\n\nPor favor, abra um ticket de suporte selecionando a negociação na tela de negociações em aberto e depois pressionando "\alt+o\" ou \"option+o\". +popup.warning.moneroConnection=Houve um problema ao conectar-se à rede Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1985,8 +1991,8 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=Identificação pessoal -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telefone.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. O valor mínimo de saque é de 10 euros e valor máximo é de 600 EUR. Para saques repetidos é de 3000 euros por destinatário por dia e 6000 euros por destinatário por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nO valor de saque deve ser um múltiplo de 10 euros, pois você não pode sacar notas diferentes de uma ATM. Esse valor em XMR será ajustado na telas de criar e aceitar ofertas para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. @@ -1998,7 +2004,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Certifique-se de que o seu banco permite a realização de depósitos em espécie na conta de terceiros. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Por favor, esteja ciente de que o Cash App tem um risco maior de estorno do que a maioria das transferências bancárias. @@ -2032,7 +2038,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação de fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre utilize métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 0ec7c93184..451dc5ddbd 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Abaixo % do preço de mercado shared.aboveInPercent=Acima % do preço de mercado shared.enterPercentageValue=Insira % do valor shared.OR=OU -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Esperando pelos fundos... shared.TheXMRBuyer=O comprador de XMR shared.You=Você @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Filtro shared.enabled=Enabled @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=Rever: Colocar oferta para {0} monero +createOffer.placeOfferButton.buy=Revisar: Criar oferta para comprar XMR com {0} +createOffer.placeOfferButton.sell=Revisar: Criar oferta para vender XMR por {0} createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n @@ -524,7 +520,8 @@ takeOffer.success.info=Você pode ver o estado de seu negócio em \"Portefólio/ takeOffer.error.message=Ocorreu um erro ao aceitar a oferta .\n\n{0} # new entries -takeOffer.takeOfferButton=Rever: Colocar oferta para {0} monero +takeOffer.takeOfferButton.buy=Revisar: Aceitar oferta para comprar XMR com {0} +takeOffer.takeOfferButton.sell=Revisar: Aceitar oferta para vender XMR por {0} takeOffer.noPriceFeedAvailable=Você não pode aceitar aquela oferta pois ela utiliza uma percentagem do preço baseada no preço de mercado, mas o feed de preços está indisponível no momento. takeOffer.takeOfferFundWalletInfo.headline=Financiar seu negócio # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Esperando confirmação da blockchain +portfolio.pending.step2_buyer.additionalConf=Os depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProceda antecipadamente por sua própria conta e risco. portfolio.pending.step2_buyer.startPayment=Iniciar pagamento portfolio.pending.step2_seller.waitPaymentSent=Aguardar até que o pagamento inicie portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar até que o pagamento chegue @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money O # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Por favor contacte o vendedor de XMR pelo contacto fornecido e marque um encontro para pagar {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Iniciar pagamento usando {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Informação do contacto do comp portfolio.pending.step2_seller.waitPayment.msg=A transação de depósito tem pelo menos uma confirmação da blockchain.\nVocê precisa esperar até que o comprador de XMR inicie o pagamento {0}. portfolio.pending.step2_seller.warn=O comprador do XMR ainda não efetuou o pagamento de {0}.\nVocê precisa esperar até que eles tenham iniciado o pagamento.\nSe o negócio não for concluído em {1}, o árbitro irá investigar. portfolio.pending.step2_seller.openForDispute=O comprador de XMR não iniciou o seu pagamento!\nO período máx. permitido para o negócio acabou.\nVocê pode esperar e dar mais tempo ao seu par de negociação ou entrar em contacto com o mediador para assistência. -tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID ''{0}'' +tradeChat.chatWindowTitle=Janela de chat para o negócio com o ID '{0}' tradeChat.openChat=Abrir janela de chat tradeChat.rules=Você pode comunicar com o seu par de negociação para resolver problemas com este negócio.\nNão é obrigatório responder no chat.\nSe algum negociante infringir alguma das regras abaixo, abra uma disputa e reporte-o ao mediador ou ao árbitro.\n\nRegras do chat:\n\t● Não envie nenhum link (risco de malware). Você pode enviar o ID da transação e o nome de um explorador de blocos.\n\t● Não envie as suas palavras-semente, chaves privadas, senhas ou outra informação sensitiva!\n\t● Não encoraje negócios fora do Haveno (sem segurança).\n\t● Não engaje em nenhuma forma de scams de engenharia social.\n\t● Se um par não responde e prefere não comunicar pelo chat, respeite a sua decisão.\n\t● Mantenha o âmbito da conversa limitado ao negócio. Este chat não é um substituto para o messenger ou uma caixa para trolls.\n\t● Mantenha a conversa amigável e respeitosa. @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=Se tiver algum problema você pode tentar portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Verificar o pagamento portfolio.pending.support.headline.periodOver=O período de negócio acabou +portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente +portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência. portfolio.pending.mediationRequested=Mediação solicitada portfolio.pending.refundRequested=Reembolso pedido @@ -812,14 +812,14 @@ portfolio.pending.mediationResult.info.peerAccepted=O seu par de negócio aceito portfolio.pending.mediationResult.button=Ver a resolução proposta portfolio.pending.mediationResult.popup.headline=Resultado da mediação para o negócio com o ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=O seu par de negócio aceitou a sugestão do mediador para o negócio {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Rejeitar e solicitar arbitragem portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Uma transação de depósito está faltando.\n\nEssa transação é necessária para concluir a negociação. Certifique-se de que sua carteira esteja totalmente sincronizada com a blockchain do Monero.\n\nVocê pode mover esta negociação para a seção "Negociações com Falha" para desativá-la. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitragem Antiga support.tab.ArbitratorsSupportTickets=Bilhetes de {0} -support.filter=Search disputes -support.filter.prompt=Insira o ID do negócio, data, endereço onion ou dados da conta support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -988,7 +986,7 @@ support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVers support.peerOpenedTicket=O seu par de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedDispute=O seu par de negociação solicitou uma disputa.\n\n{0}\n\nVersão Haveno: {1} support.peerOpenedDisputeForMediation=O seu par de negociação solicitou uma mediação.\n\n{0}\n\nVersão Haveno: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? @@ -1033,6 +1031,7 @@ setting.preferences.displayOptions=Mostrar opções setting.preferences.showOwnOffers=Mostrar as minhas próprias ofertas no livro de ofertas setting.preferences.useAnimations=Usar animações setting.preferences.useDarkMode=Usar o modo escuro +setting.preferences.useLightMode=Usar modo claro setting.preferences.sortWithNumOffers=Ordenar listas de mercado por nº de ofertas/negociações: setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1048,6 +1047,9 @@ settings.preferences.editCustomExplorer.name=Nome settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Nova funcionalidade de privacidade de dados +settings.preferences.sensitiveDataRemoval.msg=Para proteger a privacidade sua e de outros negociadores, o Haveno pretende remover dados sensíveis de negociações antigas. Isso é particularmente importante para negociações com moedas fiduciárias que podem incluir detalhes de conta bancária.\n\nRecomenda-se definir o prazo o mais baixo possível, por exemplo, 60 dias. Isso significa que negociações com mais de 60 dias terão os dados sensíveis removidos, desde que estejam concluídas. Negociações concluídas podem ser encontradas na aba Portfólio / Histórico. + settings.net.xmrHeader=Rede Monero settings.net.p2pHeader=Rede do Haveno settings.net.onionAddressLabel=O meu endereço onion @@ -1114,19 +1116,19 @@ setting.about.subsystems.label=Versão de subsistemas setting.about.subsystems.val=Versão da rede: {0}; Versão de mensagem P2P: {1}; Versão da base de dados local: {2}; Versão do protocolo de negócio: {3} setting.about.shortcuts=Atalhos -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' ou ''alt + {0}'' ou ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' ou 'alt + {0}' ou 'cmd + {0}' setting.about.shortcuts.menuNav=Navigar o menu principal setting.about.shortcuts.menuNav.value=Para navigar o menu principal pressione:: 'Ctrl' ou 'alt' ou 'cmd' juntamente com uma tecla numérica entre '1-9' setting.about.shortcuts.close=Fechar o Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' ou ''cmd + {0}'' ou ''Ctrl + {1}'' ou ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' ou 'cmd + {0}' ou 'Ctrl + {1}' ou 'cmd + {1}' setting.about.shortcuts.closePopup=Fechar popup ou janela de diálogo setting.about.shortcuts.closePopup.value=Tecla "ESCAPE" setting.about.shortcuts.chatSendMsg=Enviar uma mensagem ao negociador -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' ou ''alt + ENTER'' ou ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' ou 'alt + ENTER' ou 'cmd + ENTER' setting.about.shortcuts.openDispute=Abrir disputa setting.about.shortcuts.openDispute.value=Selecionar negócio pendente e clicar: {0} @@ -1317,7 +1319,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Tipo de oferta account.notifications.marketAlert.message.title=Alerta de oferta account.notifications.marketAlert.message.msg.below=abaixo de account.notifications.marketAlert.message.msg.above=acima de -account.notifications.marketAlert.message.msg=Uma nova ''{0} {1}'' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento ''{5}'' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. +account.notifications.marketAlert.message.msg=Uma nova '{0} {1}' com o preço de {2} ({3} {4} preço de mercado) e método de pagamento '{5}' foi publicada no livro de ofertas do Haveno.\nID da oferta: {6}. account.notifications.priceAlert.message.title=Alerta de preço para {0} account.notifications.priceAlert.message.msg=O teu alerta de preço foi desencadeado. O preço atual de {0} é de {1} {2} account.notifications.noWebCamFound.warning=Nenhuma webcam foi encontrada.\n\nPor favor use a opção email para enviar o token e a chave de criptografia do seu telemóvel para o programa da Haveno. @@ -1468,11 +1470,14 @@ offerDetailsWindow.countryBank=País do banco do ofertante offerDetailsWindow.commitment=Compromisso offerDetailsWindow.agree=Eu concordo offerDetailsWindow.tac=Termos e condições -offerDetailsWindow.confirm.maker=Confirmar: Criar oferta para {0} monero -offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero +offerDetailsWindow.confirm.maker.buy=Confirmar: Criar oferta para comprar XMR com {0} +offerDetailsWindow.confirm.maker.sell=Confirmar: Criar oferta para vender XMR por {0} +offerDetailsWindow.confirm.taker.buy=Confirmar: Aceitar oferta para comprar XMR com {0} +offerDetailsWindow.confirm.taker.sell=Confirmar: Aceitar oferta para vender XMR por {0} offerDetailsWindow.creationDate=Data de criação offerDetailsWindow.makersOnion=Endereço onion do ofertante offerDetailsWindow.challenge=Passphrase da oferta +offerDetailsWindow.challenge.copy=Copiar frase secreta para compartilhar com seu parceiro qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1558,7 +1563,7 @@ torNetworkSettingWindow.deleteFiles.button=Apagar ficheiros de Tor desatualizado torNetworkSettingWindow.deleteFiles.progress=Desligar Tor em progresso torNetworkSettingWindow.deleteFiles.success=Ficheiros de Tor desatualizados apagados com sucesso. Por favor reinicie. torNetworkSettingWindow.bridges.header=O Tor está bloqueado? -torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu fornecedor de internet ou pelo seu país, você pode tentar usar pontes Tor.\nVisite a página web do Tor em: https://bridges.torproject.org/bridges para saber mais sobre pontes e transportes conectáveis. +torNetworkSettingWindow.bridges.info=Se o Tor estiver bloqueado pelo seu fornecedor de internet ou pelo seu país, você pode tentar usar pontes Tor.\nVisite a página web do Tor em: https://bridges.torproject.org para saber mais sobre pontes e transportes conectáveis. feeOptionWindow.headline=Escolha a moeda para o pagamento da taxa de negócio feeOptionWindow.info=Pode escolher pagar a taxa de negócio em BSQ ou em XMR. Se escolher BSQ tira proveito da taxa de negócio descontada. @@ -1618,6 +1623,7 @@ popup.warning.noPriceFeedAvailable=Não há feed de preço disponível para essa popup.warning.sendMsgFailed=Enviar mensagem para seu par de negociação falhou.\nPor favor, tente novamente e se continuar a falhar relate um erro. popup.warning.messageTooLong=Sua mensagem excede o tamanho máx. permitido. Por favor enviá-la em várias partes ou carregá-la utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você trancou fundos de um negócio falhado..\nSaldo trancado: {0} \nEndereço da tx de Depósito: {1}\nID de negócio: {2}.\n\nPor favor abra um bilhete de apoio selecionando o negócio no ecrã de negócios abertos e pressione \"alt + o\" ou \"option + o\"." +popup.warning.moneroConnection=Houve um problema ao conectar-se à rede Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1975,8 +1981,8 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=ID pessoal -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Ao usar o HalCash, o comprador de XMR precisa enviar ao vendedor de XMR o código HalCash através de uma mensagem de texto do seu telemóvel.\n\nPor favor, certifique-se de não exceder a quantia máxima que seu banco lhe permite enviar com o HalCash. A quantia mín. de levantamento é de 10 euros e a quantia máx. é de 600 EUR. Para levantamentos repetidos é de 3000 euros por recipiente por dia e 6000 euros por recipiente por mês. Por favor confirme esses limites com seu banco para ter certeza de que eles usam os mesmos limites mencionados aqui.\n\nA quantia de levantamento deve ser um múltiplo de 10 euros, pois você não pode levantar outras quantias de uma ATM. A interface do utilizador no ecrã para criar oferta e aceitar ofertas ajustará a quantia de XMR para que a quantia de EUR esteja correta. Você não pode usar o preço com base no mercado, pois o valor do EUR estaria mudando com a variação dos preços.\n\nEm caso de disputa, o comprador de XMR precisa fornecer a prova de que enviou o EUR. @@ -1988,7 +1994,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Por favor, confirme que seu banco permite-lhe enviar depósitos em dinheiro para contas de outras pessoas. Por exemplo, o Bank of America e o Wells Fargo não permitem mais esses depósitos. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Esteja ciente de que o Cash App tem um risco de estorno maior do que a maioria das transferências bancárias. @@ -2022,7 +2028,7 @@ payment.japan.recipient=Nome payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Para sua proteção, desaconselhamos fortemente o uso de PINs do Paysafecard para pagamento.\n\n\ Transações feitas por PINs não podem ser verificadas de forma independente para resolução de disputas. Se ocorrer um problema, a recuperação dos fundos pode não ser possível.\n\n\ Para garantir a segurança das transações com resolução de disputas, sempre use métodos de pagamento que forneçam registros verificáveis. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 46828f97b1..25e5b0536d 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=% ниже рыночного курса shared.aboveInPercent=% выше рыночного курса shared.enterPercentageValue=Ввести величину в % shared.OR=ИЛИ -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Ожидание средств... shared.TheXMRBuyer=Покупатель ВТС shared.You=Вы @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Фильтр shared.enabled=Enabled @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Купить XMR с помощью: offerbook.sellXmrFor=Продать XMR за: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} дн. offerbook.timeSinceSigning.notSigned.noNeed=Н/Д @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Этот способ оплаты временно ограничен до {0} до {1}, поскольку все покупатели имеют новые аккаунты.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Ваше предложение будет ограничено для покупателей с подписанными и старыми аккаунтами, потому что оно превышает {0}.\n\n{1} @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=Проверка: разместить предложение {0} биткойн +createOffer.placeOfferButton.buy=Проверка: Создать предложение на покупку XMR за {0} +createOffer.placeOfferButton.sell=Проверка: Создать предложение на продажу XMR за {0} createOffer.createOfferFundWalletInfo.headline=Обеспечить своё предложение # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n @@ -524,7 +520,8 @@ takeOffer.success.info=Статус вашей сделки отображает takeOffer.error.message=Ошибка при принятии предложения:\n\n{0} # new entries -takeOffer.takeOfferButton=Проверка: принять предложение {0} биткойн +takeOffer.takeOfferButton.buy=Проверка: Принять предложение на покупку XMR за {0} +takeOffer.takeOfferButton.sell=Проверка: Принять предложение на продажу XMR за {0} takeOffer.noPriceFeedAvailable=Нельзя принять это предложение, поскольку в нем используется процентный курс на основе рыночного курса, источник которого недоступен. takeOffer.takeOfferFundWalletInfo.headline=Обеспечьте свою сделку # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Ожидание подтверждения в блокчейне +portfolio.pending.step2_buyer.additionalConf=Депозиты достигли 10 подтверждений.\nДля дополнительной безопасности мы рекомендуем дождаться {0} подтверждений перед отправкой платежа.\nРанее действия осуществляются на ваш страх и риск. portfolio.pending.step2_buyer.startPayment=Сделать платеж portfolio.pending.step2_seller.waitPaymentSent=Дождитесь начала платежа portfolio.pending.step3_buyer.waitPaymentArrived=Дождитесь получения платежа @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Свяжитесь с продавцом XMR с помощью указанных контактных данных и договоритесь о встрече для оплаты {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Начать оплату, используя {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Контактная инфор portfolio.pending.step2_seller.waitPayment.msg=Депозитная транзакция подтверждена в блокчейне не менее одного раза.\nДождитесь начала платежа в {0} покупателем XMR. portfolio.pending.step2_seller.warn=Покупатель XMR все еще не завершил платеж в {0}.\nДождитесь начала оплаты.\nЕсли сделка не завершится {1}, арбитр начнет разбирательство. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +713,7 @@ portfolio.pending.step3_seller.westernUnion=Покупатель обязан о portfolio.pending.step3_seller.halCash=Покупатель должен отправить вам код HalCash в текстовом сообщении. Кроме того, вы получите сообщение от HalCash с информацией, необходимой для снятия EUR в банкомате, поддерживающем HalCash.\n\nПосле того, как вы заберете деньги из банкомата, подтвердите получение платежа в приложении! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Подтвердите получение платежа @@ -737,7 +735,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Вы получили платеж в {0} от своего контрагента?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Подтвердите получение платежа @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Время сделки истекло +portfolio.pending.support.headline.depositTxMissing=Отсутствует депозитная транзакция +portfolio.pending.support.depositTxMissing=Для этой сделки отсутствует депозитная транзакция. Откройте тикет в службу поддержки, чтобы связаться с арбитром для получения помощи. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested @@ -811,15 +811,15 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Отсутствует транзакция депозита.\n\nЭта транзакция необходима для завершения сделки. Пожалуйста, убедитесь, что ваш кошелёк полностью синхронизирован с блокчейном Monero.\n\nВы можете переместить эту сделку в раздел "Неудачные сделки", чтобы деактивировать её. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Введите идентификатор сделки, дату, onion-адрес или данные учётной записи support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -979,7 +977,7 @@ support.sellerMaker=Продавец ВТС/мейкер support.buyerTaker=Покупатель ВТС/тейкер support.sellerTaker=Продавец XMR/тейкер -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Системное сообщение: {0} support.youOpenedTicket=Вы запросили поддержку.\n\n{0}\n\nВерсия Haveno: {1} support.youOpenedDispute=Вы начали спор.\n\n{0}\n\nВерсия Haveno: {1} @@ -987,8 +985,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=Добавить альткойн setting.preferences.displayOptions=Параметры отображения setting.preferences.showOwnOffers=Показать мои предложения в списке предложений setting.preferences.useAnimations=Использовать анимацию -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=Использовать тёмный режим +setting.preferences.useLightMode=Использовать светлый режим setting.preferences.sortWithNumOffers=Сортировать списки по кол-ву предложений/сделок setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1047,6 +1046,9 @@ settings.preferences.editCustomExplorer.name=Имя settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Новая функция защиты данных +settings.preferences.sensitiveDataRemoval.msg=Для защиты вашей и других трейдеров конфиденциальности Haveno намерен удалять конфиденциальные данные из старых сделок. Это особенно важно для сделок с фиатной валютой, которые могут включать данные банковских счетов.\n\nРекомендуется установить минимальный срок, например, 60 дней. Это означает, что сделки старше 60 дней будут очищены от конфиденциальных данных, при условии, что они завершены. Завершённые сделки находятся во вкладке «Портфель / История». + settings.net.xmrHeader=Сеть Биткойн settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Мой onion-адрес @@ -1113,19 +1115,19 @@ setting.about.subsystems.label=Версии подсистем setting.about.subsystems.val=Версия сети: {0}; версия P2P-сообщений: {1}; версия локальной базы данных: {2}; версия торгового протокола: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1200,7 +1202,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Ваши альткойн-счета -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Я понимаю и подтверждаю, что знаю, какой кошелёк нужно использовать. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1469,11 +1471,14 @@ offerDetailsWindow.countryBank=Страна банка мейкера offerDetailsWindow.commitment=Обязательство offerDetailsWindow.agree=Подтверждаю offerDetailsWindow.tac=Пользовательское соглашение -offerDetailsWindow.confirm.maker=Подтвердите: разместить предложение {0} биткойн -offerDetailsWindow.confirm.taker=Подтвердите: принять предложение {0} биткойн +offerDetailsWindow.confirm.maker.buy=Подтверждение: Создать предложение на покупку XMR за {0} +offerDetailsWindow.confirm.maker.sell=Подтверждение: Создать предложение на продажу XMR за {0} +offerDetailsWindow.confirm.taker.buy=Подтверждение: Принять предложение на покупку XMR за {0} +offerDetailsWindow.confirm.taker.sell=Подтверждение: Принять предложение на продажу XMR за {0} offerDetailsWindow.creationDate=Дата создания offerDetailsWindow.makersOnion=Onion-адрес мейкера offerDetailsWindow.challenge=Пароль предложения +offerDetailsWindow.challenge.copy=Скопируйте кодовую фразу, чтобы поделиться с партнёром qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1559,7 +1564,7 @@ torNetworkSettingWindow.deleteFiles.button=Удалить устаревшие torNetworkSettingWindow.deleteFiles.progress=Tor завершает работу torNetworkSettingWindow.deleteFiles.success=Устаревшие файлы Tor успешно удалены. Пожалуйста, перезапустите приложение. torNetworkSettingWindow.bridges.header=Tor сеть заблокирована? -torNetworkSettingWindow.bridges.info=Если Tor заблокирован вашим интернет-провайдером или правительством, попробуйте использовать мосты Tor.\nПосетите веб-страницу Tor по адресу: https://bridges.torproject.org/bridges, чтобы узнать больше о мостах и подключаемых транспортных протоколах. +torNetworkSettingWindow.bridges.info=Если Tor заблокирован вашим интернет-провайдером или правительством, попробуйте использовать мосты Tor.\nПосетите веб-страницу Tor по адресу: https://bridges.torproject.org, чтобы узнать больше о мостах и подключаемых транспортных протоколах. feeOptionWindow.headline=Выберите валюту для оплаты торгового сбора feeOptionWindow.info=Вы можете оплатить комиссию за сделку в BSQ или XMR. Если вы выберите BSQ, то сумма комиссии будет ниже. @@ -1619,6 +1624,7 @@ popup.warning.noPriceFeedAvailable=Источник рыночного курс popup.warning.sendMsgFailed=Не удалось отправить сообщение вашему контрагенту .\nПопробуйте еще раз, и если неисправность повторится, сообщите о ней. popup.warning.messageTooLong=Ваше сообщение превышает макс. разрешённый размер. Разбейте его на несколько частей или загрузите в веб-приложение для работы с отрывками текста, например https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." +popup.warning.moneroConnection=Возникла проблема с подключением к сети Monero.\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1680,8 +1686,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1976,8 +1982,8 @@ payment.accountType=Тип счёта payment.checking=Текущий payment.savings=Сберегательный payment.personalId=Личный идентификатор -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Используя HalCash, покупатель XMR обязуется отправить продавцу XMR код HalCash через СМС с мобильного телефона.\n\nУбедитесь, что не вы не превысили максимальную сумму, которую ваш банк позволяет отправить с HalCash. Минимальная сумма на вывод средств составляет 10 EUR, а и максимальная — 600 EUR. При повторном выводе средств лимит составляет 3000 EUR на получателя в день и 6000 EUR на получателя в месяц. Просьба сверить эти лимиты с вашим банком и убедиться, что лимиты банка соответствуют лимитам, указанным здесь.\n\nВыводимая сумма должна быть кратна 10 EUR, так как другие суммы снять из банкомата невозможно. Приложение само отрегулирует сумму XMR, чтобы она соответствовала сумме в EUR, во время создания или принятия предложения. Вы не сможете использовать текущий рыночный курс, так как сумма в EUR будет меняться с изменением курса.\n\nВ случае спора покупателю XMR необходимо предоставить доказательство отправки EUR. @@ -1989,7 +1995,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Обратите внимание, что Cash App имеет более высокий риск возврата платежей, чем большинство банковских переводов. @@ -2024,7 +2030,7 @@ payment.japan.recipient=Имя payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Для вашей защиты мы настоятельно не рекомендуем использовать PIN-коды Paysafecard для платежей.\n\n\ Транзакции, выполненные с помощью PIN-кодов, не могут быть независимо подтверждены для разрешения споров. В случае возникновения проблемы возврат средств может быть невозможен.\n\n\ Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 56fe9e67c9..8e8cd7afec 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=ต่ำกว่า % จากราคาตลาด shared.aboveInPercent=สูงกว่า % จากราคาตาด shared.enterPercentageValue=เข้าสู่ % ตามมูลค่า shared.OR=หรือ -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=กำลังรอเงิน ... shared.TheXMRBuyer=ผู้ซื้อ XMR shared.You=คุณ @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=ตัวกรอง shared.enabled=Enabled @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=ซื้อ XMR ด้วย: offerbook.sellXmrFor=ขาย XMR สำหรับ: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} วัน offerbook.timeSinceSigning.notSigned.noNeed=ไม่พร้อมใช้งาน @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=วิธีการชำระเงินนี้ถูก จำกัด ชั่วคราวไปยัง {0} จนถึง {1} เนื่องจากผู้ซื้อทุกคนมีบัญชีใหม่\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=ข้อเสนอของคุณจะถูก จำกัด เฉพาะผู้ซื้อที่มีบัญชีที่ได้ลงนามและมีอายุ เนื่องจากมันเกิน {0}.\n\n{1} @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=รีวิว: ใส่ข้อเสนอไปยัง {0} บิตคอย +createOffer.placeOfferButton.buy=ตรวจสอบ: สร้างข้อเสนอเพื่อซื้อ XMR ด้วย {0} +createOffer.placeOfferButton.sell=ตรวจสอบ: สร้างข้อเสนอเพื่อขาย XMR เป็น {0} createOffer.createOfferFundWalletInfo.headline=เงินทุนสำหรับข้อเสนอของคุณ # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} @@ -488,7 +484,7 @@ createOffer.currencyForFee=ค่าธรรมเนียมการซื createOffer.setDeposit=Set buyer's security deposit (%) createOffer.setDepositAsBuyer=Set my security deposit as buyer (%) createOffer.setDepositForBothTraders=Set both traders' security deposit (%) -createOffer.securityDepositInfo=Your buyer''s security deposit will be {0} +createOffer.securityDepositInfo=Your buyer's security deposit will be {0} createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} createOffer.minSecurityDepositUsed=เงินประกันความปลอดภัยขั้นต่ำถูกใช้ createOffer.buyerAsTakerWithoutDeposit=ไม่ต้องวางมัดจำจากผู้ซื้อ (ป้องกันด้วยรหัสผ่าน) @@ -524,7 +520,8 @@ takeOffer.success.info=คุณสามารถดูสถานะการ takeOffer.error.message=เกิดข้อผิดพลาดขณะรับข้อเสนอ\n\n{0} # new entries -takeOffer.takeOfferButton=รีวิว: รับข้อเสนอจาก {0} monero +takeOffer.takeOfferButton.buy=ตรวจสอบ: ยอมรับข้อเสนอเพื่อซื้อ XMR ด้วย {0} +takeOffer.takeOfferButton.sell=ตรวจสอบ: ยอมรับข้อเสนอเพื่อขาย XMR เป็น {0} takeOffer.noPriceFeedAvailable=คุณไม่สามารถรับข้อเสนอดังกล่าวเนื่องจากใช้ราคาร้อยละตามราคาตลาด แต่ไม่มีฟีดราคาที่พร้อมใช้งาน takeOffer.takeOfferFundWalletInfo.headline=ทุนการซื้อขายของคุณ # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=รอการยืนยันของบล็อกเชน +portfolio.pending.step2_buyer.additionalConf=ยอดฝากถึง 10 การยืนยันแล้ว\nเพื่อความปลอดภัยเพิ่มเติม เราแนะนำให้รอ {0} การยืนยันก่อนทำการชำระเงิน\nดำเนินการล่วงหน้าตามความเสี่ยงของคุณเอง portfolio.pending.step2_buyer.startPayment=เริ่มการชำระเงิน portfolio.pending.step2_seller.waitPaymentSent=รอจนกว่าการชำระเงินจะเริ่มขึ้น portfolio.pending.step3_buyer.waitPaymentArrived=รอจนกว่าจะถึงการชำระเงิน @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธน # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=กรุณาติดต่อผู้ขายของ XMR ตามรายชื่อที่ได้รับและนัดประชุมเพื่อจ่ายเงิน {0}\n\n portfolio.pending.step2_buyer.startPaymentUsing=เริ่มต้นการชำระเงินโดยใช้ {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=ข้อมูลการต portfolio.pending.step2_seller.waitPayment.msg=ธุรกรรมการฝากเงินมีการยืนยันบล็อกเชนอย่างน้อยหนึ่งรายการ\nคุณต้องรอจนกว่าผู้ซื้อ XMR จะเริ่มการชำระเงิน {0} portfolio.pending.step2_seller.warn=ผู้ซื้อ XMR ยังไม่ได้ทำ {0} การชำระเงิน\nคุณต้องรอจนกว่าผู้ซื้อจะเริ่มชำระเงิน\nหากการซื้อขายยังไม่เสร็จสิ้นในวันที่ {1} ผู้ไกล่เกลี่ยจะดำเนินการตรวจสอบ portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +713,7 @@ portfolio.pending.step3_seller.westernUnion=ผู้ซื้อต้องส portfolio.pending.step3_seller.halCash=ผู้ซื้อต้องส่งข้อความรหัส HalCash ให้คุณ ในขณะเดียวกันคุณจะได้รับข้อความจาก HalCash พร้อมกับคำขอข้อมูลจำเป็นในการถอนเงินยูโรุจากตู้เอทีเอ็มที่รองรับ HalCash \n\n หลังจากที่คุณได้รับเงินจากตู้เอทีเอ็มโปรดยืนยันใบเสร็จรับเงินจากการชำระเงินที่นี่ ! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=ใบเสร็จยืนยันการชำระเงิน @@ -737,7 +735,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=คุณได้รับ {0} การชำระเงินจากคู่ค้าของคุณหรือไม่\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=ยืนยันว่าคุณได้รับการชำระเงินแล้ว @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over +portfolio.pending.support.headline.depositTxMissing=การฝากธุรกรรมหายไป +portfolio.pending.support.depositTxMissing=รายการฝากสำหรับการซื้อขายนี้หายไป กรุณาเปิดตั๋วสนับสนุนเพื่อติดต่อผู้ตัดสินเพื่อขอความช่วยเหลือ portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested @@ -811,15 +811,15 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=ธุรกรรมเงินมัดจำหายไป\n\nธุรกรรมนี้จำเป็นสำหรับการดำเนินการซื้อขายให้เสร็จสมบูรณ์ กรุณาตรวจสอบให้แน่ใจว่า Wallet ของคุณได้ซิงค์กับบล็อกเชน Monero อย่างสมบูรณ์แล้ว\n\nคุณสามารถย้ายการซื้อขายนี้ไปยังส่วน "การซื้อขายที่ล้มเหลว" เพื่อปิดการใช้งานได้ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -926,8 +926,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Enter trade ID, date, onion address or account data support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -979,7 +977,7 @@ support.sellerMaker= XMR ผู้ขาย/ ผู้สร้าง support.buyerTaker=XMR ผู้ซื้อ / ผู้รับ support.sellerTaker=XMR ผู้ขาย / ผู้รับ -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=ระบบข้อความ: {0} support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nHaveno version: {1} support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nHaveno version: {1} @@ -987,8 +985,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1031,7 +1029,8 @@ setting.preferences.addCrypto=เพิ่ม crypto setting.preferences.displayOptions=แสดงตัวเลือกเพิ่มเติม setting.preferences.showOwnOffers=แสดงข้อเสนอของฉันเองในสมุดข้อเสนอ setting.preferences.useAnimations=ใช้ภาพเคลื่อนไหว -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=ใช้โหมดมืด +setting.preferences.useLightMode=ใช้โหมดสว่าง setting.preferences.sortWithNumOffers=จัดเรียงรายการโดยเลขของข้อเสนอ / การซื้อขาย setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1047,6 +1046,9 @@ settings.preferences.editCustomExplorer.name=ชื่อ settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=คุณสมบัติความเป็นส่วนตัวข้อมูลใหม่ +settings.preferences.sensitiveDataRemoval.msg=เพื่อปกป้องความเป็นส่วนตัวของคุณและผู้ซื้อขายอื่นๆ Haveno ตั้งใจที่จะลบข้อมูลที่ละเอียดอ่อนจากการซื้อขายเก่า ซึ่งมีความสำคัญอย่างยิ่งสำหรับการซื้อขายฟิอาทที่อาจมีรายละเอียดบัญชีธนาคาร\n\nแนะนำให้ตั้งค่านี้ให้ต่ำที่สุดเท่าที่จะเป็นไปได้ เช่น 60 วัน ซึ่งหมายความว่าการซื้อขายที่เกิน 60 วันจะถูกลบข้อมูลที่ละเอียดอ่อน ตราบใดที่การซื้อขายนั้นเสร็จสมบูรณ์ การซื้อขายที่เสร็จสมบูรณ์สามารถดูได้ในแท็บพอร์ตโฟลิโอ / ประวัติ + settings.net.xmrHeader=เครือข่าย Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน @@ -1113,19 +1115,19 @@ setting.about.subsystems.label=เวอร์ชั่นของระบบ setting.about.subsystems.val=เวอร์ชั่นของเครือข่าย: {0}; เวอร์ชั่นข้อความ P2P: {1}; เวอร์ชั่นฐานข้อมูลท้องถิ่น: {2}; เวอร์ชั่นโปรโตคอลการซื้อขาย: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1200,7 +1202,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=บัญชี crypto (เหรียญทางเลือก) ของคุณ -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=ฉันเข้าใจและยืนยันว่าฉันรู้ว่า wallet ใดที่ฉันต้องการใช้ # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1469,11 +1471,14 @@ offerDetailsWindow.countryBank=ประเทศของธนาคารข offerDetailsWindow.commitment=ข้อผูกมัด offerDetailsWindow.agree=ฉันเห็นด้วย offerDetailsWindow.tac=ข้อตกลงและเงื่อนไข -offerDetailsWindow.confirm.maker=ยืนยัน: ยื่นข้อเสนอไปยัง{0} บิทคอยน์ -offerDetailsWindow.confirm.taker=ยืนยัน: รับข้อเสนอไปยัง {0} บิทคอยน์ +offerDetailsWindow.confirm.maker.buy=ยืนยัน: สร้างข้อเสนอเพื่อซื้อ XMR ด้วย {0} +offerDetailsWindow.confirm.maker.sell=ยืนยัน: สร้างข้อเสนอเพื่อขาย XMR เป็น {0} +offerDetailsWindow.confirm.taker.buy=ยืนยัน: ยอมรับข้อเสนอเพื่อซื้อ XMR ด้วย {0} +offerDetailsWindow.confirm.taker.sell=ยืนยัน: ยอมรับข้อเสนอเพื่อขาย XMR เป็น {0} offerDetailsWindow.creationDate=วันที่สร้าง offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง offerDetailsWindow.challenge=รหัสผ่านสำหรับข้อเสนอ +offerDetailsWindow.challenge.copy=คัดลอกวลีรหัสเพื่อแชร์กับเพื่อนของคุณ qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1559,7 +1564,7 @@ torNetworkSettingWindow.deleteFiles.button=ลบไฟล์ Tor ที่ล torNetworkSettingWindow.deleteFiles.progress=ปิด Tor ที่กำลังดำเนินอยู่ torNetworkSettingWindow.deleteFiles.success=ไฟล์ Tor ที่ล้าสมัยถูกลบแล้ว โปรดรีสตาร์ท torNetworkSettingWindow.bridges.header=Tor ถูกบล็อกหรือไม่ -torNetworkSettingWindow.bridges.info=ถ้า Tor ถูกปิดกั้นโดยผู้ให้บริการอินเทอร์เน็ตหรือประเทศของคุณ คุณสามารถลองใช้ Tor bridges\nไปที่หน้าเว็บของ Tor ที่ https://bridges.torproject.org/bridges เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับสะพานและการขนส่งแบบ pluggable +torNetworkSettingWindow.bridges.info=ถ้า Tor ถูกปิดกั้นโดยผู้ให้บริการอินเทอร์เน็ตหรือประเทศของคุณ คุณสามารถลองใช้ Tor bridges\nไปที่หน้าเว็บของ Tor ที่ https://bridges.torproject.org เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับสะพานและการขนส่งแบบ pluggable feeOptionWindow.headline=เลือกสกุลเงินสำหรับการชำระค่าธรรมเนียมการซื้อขาย feeOptionWindow.info=คุณสามารถเลือกที่จะชำระค่าธรรมเนียมทางการค้าใน BSQ หรือใน XMR แต่ถ้าคุณเลือก BSQ คุณจะได้รับส่วนลดค่าธรรมเนียมการซื้อขาย @@ -1619,6 +1624,7 @@ popup.warning.noPriceFeedAvailable=ไม่มีฟีดราคาสำห popup.warning.sendMsgFailed=การส่งข้อความไปยังคู่ค้าของคุณล้มเหลว\nโปรดลองอีกครั้งและหากยังคงเกิดขึ้นขึ้นเนื่อง โปรดรายงานข้อผิดพลาดต่อไป popup.warning.messageTooLong=ข้อความของคุณเกินขีดจำกัดสูงสุดที่อนุญาต โปรดแบ่งส่งเป็นหลายส่วนหรืออัปโหลดไปยังบริการเช่น https://pastebin.com popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." +popup.warning.moneroConnection=เกิดปัญหาในการเชื่อมต่อกับเครือข่าย Monero\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1631,7 +1637,7 @@ popup.warning.priceRelay=ราคาผลัดเปลี่ยน popup.warning.seed=รหัสลับเพื่อกู้ข้อมูล popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. A mandatory update was released which disables trading for old versions. Please check out the Haveno Forum for more information. popup.warning.noFilter=เราไม่ได้รับวัตถุกรองจากโหนดต้นทาง กรุณาแจ้งผู้ดูแลระบบเครือข่ายให้ลงทะเบียนวัตถุกรอง -popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. +popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. Please wait until the mining fees are low again or until you've accumulated more XMR to transfer. popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer with ID {0} was rejected by the Monero network.\nTransaction ID={1}.\nThe offer has been removed to avoid further problems.\nPlease go to \"Settings/Network info\" and do a SPV resync.\nFor further help please contact the Haveno support channel at the Haveno Keybase team. @@ -1680,8 +1686,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1976,8 +1982,8 @@ payment.accountType=ประเภทบัญชี payment.checking=การตรวจสอบ payment.savings=ออมทรัพย์ payment.personalId=รหัส ID ประจำตัวบุคคล -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=เมื่อมีการใช้งาน HalCash ผู้ซื้อ XMR จำเป็นต้องส่งรหัส Halcash ให้กับผู้ขายทางข้อความโทรศัพท์มือถือ\n\nโปรดตรวจสอบว่าไม่เกินจำนวนเงินสูงสุดที่ธนาคารของคุณอนุญาตให้คุณส่งด้วย HalCash จำนวนเงินขั้นต่ำในการเบิกถอนคือ 10 EUR และสูงสุดในจำนวนเงิน 600 EUR สำหรับการถอนซ้ำเป็น 3000 EUR ต่อผู้รับและต่อวัน และ 6000 EUR ต่อผู้รับและต่อเดือน โปรดตรวจสอบข้อจำกัดจากทางธนาคารคุณเพื่อให้มั่นใจได้ว่าทางธนาคารได้มีการใช้มาตรฐานข้อกำหนดเดียวกันกับดังที่ระบุไว้ ณ ที่นี่\n\nจำนวนเงินที่ถอนจะต้องเป็นจำนวนเงินหลาย 10 EUR เนื่องจากคุณไม่สามารถถอนเงินอื่น ๆ ออกจากตู้เอทีเอ็มได้ UI ในหน้าจอสร้างข้อเสนอและรับข้อเสนอจะปรับจำนวนเงิน XMR เพื่อให้จำนวนเงิน EUR ถูกต้อง คุณไม่สามารถใช้ราคาตลาดเป็นจำนวนเงิน EUR ซึ่งจะเปลี่ยนแปลงไปตามราคาที่มีการปรับเปลี่ยน\n\nในกรณีที่มีข้อพิพาทผู้ซื้อ XMR ต้องแสดงหลักฐานว่าได้ส่ง EUR แล้ว @@ -1989,7 +1995,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=โปรดทราบว่า Cash App มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ @@ -2023,7 +2029,7 @@ payment.japan.recipient=ชื่อ payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=เพื่อความปลอดภัยของคุณ เราขอแนะนำอย่างยิ่งให้หลีกเลี่ยงการใช้ Paysafecard PINs ในการชำระเงิน\n\n\ ธุรกรรมที่ดำเนินการผ่าน PIN ไม่สามารถตรวจสอบได้อย่างอิสระสำหรับการระงับข้อพิพาท หากเกิดปัญหา อาจไม่สามารถกู้คืนเงินได้\n\n\ เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 28034c72a3..9d2e74e7f3 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -231,7 +226,7 @@ shared.delayedPayoutTxId=Gecikmiş ödeme işlem kimliği shared.delayedPayoutTxReceiverAddress=Gecikmiş ödeme işlemi gönderildi shared.unconfirmedTransactionsLimitReached=Şu anda çok fazla onaylanmamış işleminiz var. Lütfen daha sonra tekrar deneyin. shared.numItemsLabel=Girdi sayısı: {0} -shared.filter=Filtrele +shared.filter=Filtre shared.enabled=Etkin shared.pending=Beklemede shared.me=Ben @@ -439,7 +434,7 @@ offerbook.warning.requireUpdateToNewVersion=Sizin Haveno sürümünüz artık ti offerbook.warning.offerWasAlreadyUsedInTrade=Bu teklifi alamazsınız çünkü daha önce aldınız. \ Önceki teklif alma girişiminiz başarısız bir ticaretle sonuçlanmış olabilir. -offerbook.warning.arbitratorNotValidated=Bu teklif, hakem geçersiz olduğu için alınamaz +offerbook.warning.arbitratorNotValidated=Bu teklif kabul edilemez çünkü hakem kayıtlı değil. offerbook.warning.signatureNotValidated=Bu teklif, hakemin imzası geçersiz olduğu için alınamaz offerbook.info.sellAtMarketPrice=Piyasa fiyatından satış yapacaksınız (her dakika güncellenir). @@ -460,12 +455,8 @@ createOffer.amount.prompt=XMR miktarını girin createOffer.price.prompt=Fiyatı girin createOffer.volume.prompt={0} cinsinden miktar girin createOffer.amountPriceBox.amountDescription={0} XMR miktarı -createOffer.amountPriceBox.buy.amountDescriptionCrypto=Satılacak XMR miktarı -createOffer.amountPriceBox.sell.amountDescriptionCrypto=Satın alınacak XMR miktarı createOffer.amountPriceBox.buy.volumeDescription=Harcanacak {0} miktarı createOffer.amountPriceBox.sell.volumeDescription=Alınacak {0} miktarı -createOffer.amountPriceBox.buy.volumeDescriptionCrypto=Satılacak {0} miktarı -createOffer.amountPriceBox.sell.volumeDescriptionCrypto=Satın alınacak {0} miktarı createOffer.amountPriceBox.minAmountDescription=Minimum XMR miktarı createOffer.securityDeposit.prompt=Güvenlik teminatı createOffer.fundsBox.title=Teklifinizi finanse edin @@ -493,8 +484,8 @@ createOffer.triggerPrice.invalid.tooLow=Değer {0}'den yüksek olmalıdır createOffer.triggerPrice.invalid.tooHigh=Değer {0}'den düşük olmalıdır # new entries -createOffer.placeOfferButton=Gözden Geçir: Teklif ver {0} monero -createOffer.placeOfferButtonCrypto=Gözden Geçir: Teklif ver {0} {1} +createOffer.placeOfferButton.buy=Gözden Geçir: {0} ile XMR satın almak için teklif oluştur +createOffer.placeOfferButton.sell=Gözden Geçir: {0} karşılığında XMR satmak için teklif oluştur createOffer.createOfferFundWalletInfo.headline=Teklifinizi finanse edin # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Ticaret miktarı: {0} \n @@ -564,8 +555,8 @@ takeOffer.success.info=İşleminizin durumunu \"Portföy/Açık işlemler\" kıs takeOffer.error.message=Teklif alımı sırasında bir hata oluştu.\n\n{0} # new entries -takeOffer.takeOfferButton=İncele: {0} monero için teklifi al -takeOffer.takeOfferButtonCrypto=İncele: {0} {1} için teklifi al +takeOffer.takeOfferButton.buy=Gözden Geçir: {0} ile XMR satın alma teklifini kabul et +takeOffer.takeOfferButton.sell=Gözden Geçir: {0} karşılığında XMR satma teklifini kabul et takeOffer.noPriceFeedAvailable=Bu teklifi alamazsınız çünkü piyasa fiyatına dayalı yüzdelik bir fiyat kullanıyor ancak fiyat beslemesi mevcut değil. takeOffer.takeOfferFundWalletInfo.headline=İşleminizi finanse edin # suppress inspection "TrailingSpacesInProperty" @@ -632,6 +623,7 @@ portfolio.pending.unconfirmedTooLong=İşlem {0} üzerindeki güvence işlemleri Sorun devam ederse, Haveno desteğiyle iletişime geçin [HYPERLINK:https://matrix.to/#/#haveno:monero.social]. portfolio.pending.step1.waitForConf=Blok zinciri onaylarını bekleyin +portfolio.pending.step2_buyer.additionalConf=Mevduatlar 10 onayı ulaştı.\nEkstra güvenlik için, ödeme göndermeden önce {0} onayı beklemenizi öneririz.\nErken ilerlemek kendi riskinizdedir. portfolio.pending.step2_buyer.startPayment=Ödemeyi başlat portfolio.pending.step2_seller.waitPaymentSent=Ödeme gönderilene kadar bekle portfolio.pending.step3_buyer.waitPaymentArrived=Ödeme gelene kadar bekle @@ -752,8 +744,8 @@ portfolio.pending.step2_seller.f2fInfo.headline=Alıcının iletişim bilgileri portfolio.pending.step2_seller.waitPayment.msg=Yatırım işlemi kilidi açıldı.\nXMR alıcısının {0} ödemesini başlatmasını beklemeniz gerekiyor. portfolio.pending.step2_seller.warn=XMR alıcısı hala {0} ödemesini yapmadı.\nÖdeme başlatılana kadar beklemeniz gerekiyor.\nİşlem {1} tarihinde tamamlanmadıysa, arabulucu durumu inceleyecektir. portfolio.pending.step2_seller.openForDispute=XMR alıcısı ödemesine başlamadı!\nİşlem için izin verilen maksimum süre doldu.\nKarşı tarafa daha fazla zaman tanıyabilir veya arabulucu ile iletişime geçebilirsiniz. -disputeChat.chatWindowTitle=İşlem ID''si ile ilgili uyuşmazlık sohbet penceresi ''{0}'' -tradeChat.chatWindowTitle=İşlem ID''si ile ilgili trader sohbet penceresi ''{0}'' +disputeChat.chatWindowTitle=İşlem ID'si ile ilgili uyuşmazlık sohbet penceresi '{0}' +tradeChat.chatWindowTitle=İşlem ID'si ile ilgili trader sohbet penceresi '{0}' tradeChat.openChat=Sohbet penceresini aç tradeChat.rules=Bu işlemle ilgili olası sorunları çözmek için işlem ortağınızla iletişim kurabilirsiniz.\n\ Sohbette yanıt vermek zorunlu değildir.\n\ @@ -908,6 +900,8 @@ portfolio.pending.support.headline.getHelp=Yardıma mı ihtiyacınız var? portfolio.pending.support.button.getHelp=Tüccar Sohbetini Aç portfolio.pending.support.headline.halfPeriodOver=Ödemeyi kontrol edin portfolio.pending.support.headline.periodOver=Ticaret süresi doldu +portfolio.pending.support.headline.depositTxMissing=Eksik yatırma işlemi +portfolio.pending.support.depositTxMissing=Bu işlem için bir para yatırma işlemi eksik. Yardım almak için bir tahkimciyle iletişime geçmek üzere bir destek talebi açın. portfolio.pending.arbitrationRequested=Arabuluculuk talep edildi portfolio.pending.mediationRequested=Arabuluculuk talep edildi @@ -956,11 +950,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi. \ Teklifiniz diğer tüccarlar için hala mevcut, bu yüzden üretici ücretini kaybetmediniz. \ Bu ticareti başarısız ticaretler arasına taşıyabilirsiniz. -portfolio.pending.failedTrade.missingDepositTx=Para yatırma işlemi (2-of-2 multisig işlemi) eksik.\n\n\ - Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi ancak ticaret ücretiniz ödendi. \ - Ticaret ücretinin geri ödenmesi için burada talepte bulunabilirsiniz: \ - [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ - Bu ticareti başarısız ticaretler arasına taşımakta özgürsünüz. +portfolio.pending.failedTrade.missingDepositTx=Bir teminat işlemi eksik.\n\nBu işlem, ticareti tamamlamak için gereklidir. Lütfen cüzdanınızın Monero blok zinciri ile tamamen senkronize olduğundan emin olun.\n\nBu ticareti devre dışı bırakmak için "Başarısız İşlemler" bölümüne taşıyabilirsiniz. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Lütfen geleneksel veya kripto para ödemesini XMR satıcısına göndermeyin, çünkü gecikmiş ödeme işlemi olmadan arabuluculuk \ @@ -1124,8 +1114,6 @@ support.tab.refund.support=Geri Ödeme support.tab.arbitration.support=Arbitraj support.tab.legacyArbitration.support=Eski Arbitraj support.tab.ArbitratorsSupportTickets={0}'nin biletleri -support.filter=Uyuşmazlıkları ara -support.filter.prompt=İşlem ID'si, tarih, onion adresi veya hesap verilerini girin support.tab.SignedOffers=İmzalı Teklifler support.prompt.signedOffer.penalty.msg=Bu, üreticiden bir ceza ücreti alacak ve kalan işlem fonlarını cüzdanına iade edecektir. Göndermek istediğinizden emin misiniz?\n\n\ Teklif ID'si: {0}\n\ @@ -1299,6 +1287,7 @@ setting.preferences.displayOptions=Görüntüleme seçenekleri setting.preferences.showOwnOffers=Teklif defterinde kendi tekliflerini göster setting.preferences.useAnimations=Animasyonları kullan setting.preferences.useDarkMode=Karanlık modu kullan +setting.preferences.useLightMode=Aydınlık modu kullan setting.preferences.sortWithNumOffers=Piyasaları teklif sayısına göre sırala setting.preferences.onlyShowPaymentMethodsFromAccount=Olmayan ödeme yöntemlerini gizle setting.preferences.denyApiTaker=API kullanan alıcıları reddet @@ -1315,6 +1304,9 @@ settings.preferences.editCustomExplorer.name=İsim settings.preferences.editCustomExplorer.txUrl=İşlem URL'si settings.preferences.editCustomExplorer.addressUrl=Adres URL'si +setting.info.headline=Yeni veri gizliliği özelliği +settings.preferences.sensitiveDataRemoval.msg=Kendinizin ve diğer tacirlerin gizliliğini korumak için Haveno, eski işlemlerden hassas verileri kaldırmayı planlamaktadır. Bu, banka hesap bilgilerini içerebilecek fiat işlemleri için özellikle önemlidir.\n\nVeri silme eşiğinin mümkün olduğunca düşük, örneğin 60 gün olarak ayarlanması önerilir. Bu, 60 günden eski ve tamamlanmış işlemlerde hassas verilerin temizleneceği anlamına gelir. Tamamlanmış işlemler Portföy / Geçmiş sekmesinde bulunabilir. + settings.net.xmrHeader=Monero ağı settings.net.p2pHeader=Haveno ağı settings.net.onionAddressLabel=Onion adresim @@ -1392,19 +1384,19 @@ setting.about.subsystems.label=Alt sistemlerin sürümleri setting.about.subsystems.val=Ağ sürümü: {0}; P2P mesaj sürümü: {1}; Yerel DB sürümü: {2}; Ticaret protokolü sürümü: {3} setting.about.shortcuts=Kısayollar -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' veya ''alt + {0}'' veya ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' veya 'alt + {0}' veya 'cmd + {0}' setting.about.shortcuts.menuNav=Ana menüde gezin setting.about.shortcuts.menuNav.value=Ana menüde gezinmek için şuna basın: 'Ctrl' veya 'alt' veya 'cmd' ve '1-9' arasındaki bir sayı tuşu setting.about.shortcuts.close=Haveno'yu kapat -setting.about.shortcuts.close.value=''Ctrl + {0}'' veya ''cmd + {0}'' veya ''Ctrl + {1}'' veya ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' veya 'cmd + {0}' veya 'Ctrl + {1}' veya 'cmd + {1}' setting.about.shortcuts.closePopup=Açılır pencereyi veya iletişim penceresini kapat setting.about.shortcuts.closePopup.value='ESCAPE' tuşu setting.about.shortcuts.chatSendMsg=Trader sohbet mesajı gönder -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' veya ''alt + ENTER'' veya ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' veya 'alt + ENTER' veya 'cmd + ENTER' setting.about.shortcuts.openDispute=Uyuşmazlık aç setting.about.shortcuts.openDispute.value=Bekleyen işlemi seçin ve tıklayın: {0} @@ -1789,7 +1781,7 @@ account.notifications.marketAlert.message.title=Teklif uyarısı account.notifications.marketAlert.message.msg.below=altında account.notifications.marketAlert.message.msg.above=üstünde account.notifications.marketAlert.message.msg=Haveno teklif defterine {2} ({3} {4} piyasa fiyatı) fiyatıyla \ - ve ödeme yöntemi ''{5}'' olan yeni bir ''{0} {1}'' teklifi yayınlandı.\n\ + ve ödeme yöntemi '{5}' olan yeni bir '{0} {1}' teklifi yayınlandı.\n\ Teklif ID: {6}. account.notifications.priceAlert.message.title={0} için fiyat uyarısı account.notifications.priceAlert.message.msg=Fiyat uyarınız tetiklendi. Mevcut {0} fiyatı {1} {2} @@ -1967,13 +1959,14 @@ offerDetailsWindow.countryBank=Yapıcı'nın banka ülkesi offerDetailsWindow.commitment=Taahhüt offerDetailsWindow.agree=Kabul ediyorum offerDetailsWindow.tac=Şartlar ve koşullar -offerDetailsWindow.confirm.maker=Onayla: {0} monero teklifi yerleştir -offerDetailsWindow.confirm.makerCrypto=Onayla: {0} {1} teklifi yerleştir -offerDetailsWindow.confirm.taker=Onayla: {0} monero teklifi al -offerDetailsWindow.confirm.takerCrypto=Onayla: {0} {1} teklifi al +offerDetailsWindow.confirm.maker.buy=Onayla: {0} ile XMR satın almak için teklif oluştur +offerDetailsWindow.confirm.maker.sell=Onayla: {0} karşılığında XMR satmak için teklif oluştur +offerDetailsWindow.confirm.taker.buy=Onayla: {0} ile XMR satın alma teklifini kabul et +offerDetailsWindow.confirm.taker.sell=Onayla: {0} karşılığında XMR satma teklifini kabul et offerDetailsWindow.creationDate=Oluşturma tarihi offerDetailsWindow.makersOnion=Yapıcı'nın onion adresi offerDetailsWindow.challenge=Teklif şifresi +offerDetailsWindow.challenge.copy=Parolanızı eşinizle paylaşmak için kopyalayın qRCodeWindow.headline=QR Kodu qRCodeWindow.msg=Harici cüzdanınızdan Haveno cüzdanınızı finanse etmek için bu QR kodunu kullanın. @@ -2070,7 +2063,7 @@ torNetworkSettingWindow.deleteFiles.success=Eski Tor dosyaları başarıyla sili torNetworkSettingWindow.bridges.header=Tor engellendi mi? torNetworkSettingWindow.bridges.info=İnternet sağlayıcınız veya ülkeniz tarafından Tor engelleniyorsa, Tor köprülerini kullanmayı deneyebilirsiniz.\n\ Köprüler ve takılabilir taşıma hakkında daha fazla bilgi edinmek için: - https://bridges.torproject.org/bridges web sayfasını ziyaret edin. + https://bridges.torproject.org web sayfasını ziyaret edin. feeOptionWindow.useXMR=XMR kullan feeOptionWindow.fee={0} (≈ {1}) @@ -2147,6 +2140,7 @@ popup.warning.lockedUpFunds=Başarısız bir ticaretten kilitli fonlarınız var Yatırma tx adresi: {1}\n\ Ticaret ID'si: {2}.\n\n\ Lütfen açık ticaretler ekranında ticareti seçerek ve \"alt + o\" veya \"option + o\" tuşlarına basarak bir destek bileti açın. +popup.warning.moneroConnection=Monero ağına bağlanırken bir sorun oluştu.\n\n{0} popup.warning.makerTxInvalid=Bu teklif geçerli değil. Lütfen farklı bir teklif seçin.\n\n takeOffer.cancelButton=Teklifi iptal et diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index e6dd802dee..4db976f7ee 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -112,7 +107,7 @@ shared.belowInPercent=Thấp hơn % so với giá thị trường shared.aboveInPercent=Cao hơn % so với giá thị trường shared.enterPercentageValue=Nhập giá trị % shared.OR=HOẶC -shared.notEnoughFunds=You don''t have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. +shared.notEnoughFunds=You don't have enough funds in your Haveno wallet for this transaction—{0} is needed but only {1} is available.\n\nPlease add funds from an external wallet, or fund your Haveno wallet at Funds > Receive Funds. shared.waitingForFunds=Đợi nộp tiền... shared.TheXMRBuyer=Người mua XMR shared.You=Bạn @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=Delayed payout transaction ID shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=Bộ lọc shared.enabled=Enabled @@ -359,7 +354,7 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.buyXmrWith=Mua XMR với: offerbook.sellXmrFor=Bán XMR để: -offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. +offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers' payment accounts. offerbook.timeSinceSigning.notSigned=Not signed yet offerbook.timeSinceSigning.notSigned.ageDays={0} ngày offerbook.timeSinceSigning.notSigned.noNeed=Không áp dụng @@ -399,7 +394,7 @@ offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due t offerbook.warning.newVersionAnnouncement=With this version of the software, trading peers can verify and sign each others' payment accounts to create a network of trusted payment accounts.\n\nAfter successfully trading with a peer with a verified payment account, your payment account will be signed and trading limits will be lifted after a certain time interval (length of this interval is based on the verification method).\n\nFor more information on account signing, please see the documentation at [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer''s account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer''s account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- The buyer's account has not been signed by an arbitrator or a peer\n- The time since signing of the buyer's account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n- Your account has not been signed by an arbitrator or a peer\n- The time since signing of your account is not at least 30 days\n- The payment method for this offer is considered risky for bank chargebacks\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Phương thức thanh toán này tạm thời chỉ được giới hạn đến {0} cho đến {1} vì tất cả các người mua đều có tài khoản mới.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Đề nghị của bạn sẽ bị giới hạn chỉ đối với các người mua có tài khoản đã ký và có tuổi vì nó vượt quá {0}.\n\n{1} @@ -459,7 +454,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=Kiểm tra:: Đặt báo giá cho {0} monero +createOffer.placeOfferButton.buy=Xem lại: Tạo đề nghị mua XMR bằng {0} +createOffer.placeOfferButton.sell=Xem lại: Tạo đề nghị bán XMR lấy {0} createOffer.createOfferFundWalletInfo.headline=Nộp tiền cho báo giá của bạn # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Khoản tiền giao dịch: {0} \n @@ -524,7 +520,8 @@ takeOffer.success.info=Bạn có thể xem trạng thái giao dịch của bạn takeOffer.error.message=Có lỗi xảy ra khi nhận báo giá.\n\n{0} # new entries -takeOffer.takeOfferButton=Rà soát: Nhận báo giá cho {0} monero +takeOffer.takeOfferButton.buy=Xem lại: Chấp nhận đề nghị mua XMR bằng {0} +takeOffer.takeOfferButton.sell=Xem lại: Chấp nhận đề nghị bán XMR lấy {0} takeOffer.noPriceFeedAvailable=Bạn không thể nhận báo giá này do sử dụng giá phần trăm dựa trên giá thị trường nhưng không có giá cung cấp. takeOffer.takeOfferFundWalletInfo.headline=Nộp tiền cho giao dịch của bạn # suppress inspection "TrailingSpacesInProperty" @@ -579,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=Đợi xác nhận blockchain +portfolio.pending.step2_buyer.additionalConf=Tiền gửi đã đạt 10 xác nhận.\nĐể tăng cường bảo mật, chúng tôi khuyên bạn chờ {0} xác nhận trước khi gửi thanh toán.\nTiến hành sớm là rủi ro của bạn. portfolio.pending.step2_buyer.startPayment=Bắt đầu thanh toán portfolio.pending.step2_seller.waitPaymentSent=Đợi đến khi bắt đầu thanh toán portfolio.pending.step3_buyer.waitPaymentArrived=Đợi đến khi khoản thanh toán đến @@ -643,7 +641,7 @@ portfolio.pending.step2_buyer.postal=Hãy gửi {0} bằng \"Phiếu chuyển ti # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Vui lòng liên hệ người bán XMR và cung cấp số liên hệ và sắp xếp cuộc hẹn để thanh toán {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Thanh toán bắt đầu sử dụng {0} @@ -675,7 +673,7 @@ portfolio.pending.step2_seller.f2fInfo.headline=Thông tin liên lạc của ng portfolio.pending.step2_seller.waitPayment.msg=Giao dịch đặt cọc có ít nhất một xác nhận blockchain.\nBạn cần phải đợi cho đến khi người mua XMR bắt đầu thanh toán {0}. portfolio.pending.step2_seller.warn=Người mua XMR vẫn chưa thanh toán {0}.\nBạn cần phải đợi cho đến khi người mua bắt đầu thanh toán.\nNếu giao dịch không được hoàn thành vào {1} trọng tài sẽ điều tra. portfolio.pending.step2_seller.openForDispute=The XMR buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. -tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.chatWindowTitle=Chat window for trade with ID '{0}' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\nIt is not mandatory to reply in the chat.\nIf a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\nChat rules:\n\t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\t● Do not send your seed words, private keys, passwords or other sensitive information!\n\t● Do not encourage trading outside of Haveno (no security).\n\t● Do not engage in any form of social engineering scam attempts.\n\t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\t● Keep conversation friendly and respectful. @@ -715,7 +713,7 @@ portfolio.pending.step3_seller.westernUnion=Người mua phải gửi cho bạn portfolio.pending.step3_seller.halCash=Người mua phải gửi mã HalCash cho bạn bằng tin nhắn. Ngoài ra, bạn sẽ nhận được một tin nhắn từ HalCash với thông tin cần thiết để rút EUR từ một máy ATM có hỗ trợ HalCash. \n\nSau khi nhận được tiền từ ATM vui lòng xác nhận lại biên lai thanh toán tại đây! portfolio.pending.step3_seller.amazonGiftCard=The buyer has sent you an Amazon eGift Card by email or by text message to your mobile phone. Please redeem now the Amazon eGift Card at your Amazon account and once accepted confirm the payment receipt. -portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} +portfolio.pending.step3_seller.bankCheck=\n\nPlease also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Xác nhận đã nhận được thanh toán @@ -737,7 +735,7 @@ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Bạn đã nhận được thanh toán {0} từ Đối tác giao dịch của bạn?\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender''s name, per trade contract: {0}\n\nIf the names are not exactly the same, don''t confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n +portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the name of the sender specified on the trade contract matches the name that appears on your bank statement:\nSender's name, per trade contract: {0}\n\nIf the names are not exactly the same, don't confirm payment receipt. Instead, open a dispute by pressing \"alt + o\" or \"option + o\".\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the XMR buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Xác nhận rằng bạn đã nhận được thanh toán @@ -794,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c portfolio.pending.support.button.getHelp=Open Trader Chat portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over +portfolio.pending.support.headline.depositTxMissing=Thiếu giao dịch ký quỹ +portfolio.pending.support.depositTxMissing=Giao dịch gửi tiền cho thương vụ này bị thiếu. Mở phiếu hỗ trợ để liên hệ với trọng tài để được trợ giúp. portfolio.pending.mediationRequested=Mediation requested portfolio.pending.refundRequested=Refund requested @@ -811,15 +811,15 @@ portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediat portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? portfolio.pending.mediationResult.button=View proposed resolution portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} -portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} -portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator's suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\nYou receive: {0}\nYour trading peer receives: {1}\n\nYou can accept or reject this suggested payout.\n\nBy accepting, you sign the proposed payout transaction. If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\nIf one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nThe arbitrator may charge a small fee (fee maximum: the trader's security deposit) as compensation for their work. Both traders agreeing to the mediator's suggestion is the happy path—requesting arbitration is meant for exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion (or if the other peer is unresponsive).\n\nMore details about the new arbitration model: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator's suggested payout but it seems that your trading peer has not accepted it.\n\nOnce the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\nYou can find more details about the arbitration model at:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.missingDepositTx=Một giao dịch ký quỹ đang bị thiếu.\n\nGiao dịch này là bắt buộc để hoàn tất giao dịch. Vui lòng đảm bảo ví của bạn được đồng bộ hoàn toàn với blockchain Monero.\n\nBạn có thể chuyển giao dịch này đến mục "Giao dịch thất bại" để vô hiệu hóa nó. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] @@ -928,8 +928,6 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Search disputes -support.filter.prompt=Nhập ID giao dịch, ngày tháng, địa chỉ onion hoặc dữ liệu tài khoản support.sigCheck.button=Check signature support.sigCheck.popup.header=Verify dispute result signature @@ -981,7 +979,7 @@ support.sellerMaker=Người bán XMR/Người tạo support.buyerTaker=Người mua XMR/Người nhận support.sellerTaker=Người bán XMR/Người nhận -support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}''s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} +support.initialInfo=Please enter a description of your problem in the text field below. Add as much information as possible to speed up dispute resolution time.\n\nHere is a check list for information you should provide:\n\t● If you are the XMR buyer: Did you make the Fiat or Crypto transfer? If so, did you click the 'payment started' button in the application?\n\t● If you are the XMR seller: Did you receive the Fiat or Crypto payment? If so, did you click the 'payment received' button in the application?\n\t● Which version of Haveno are you using?\n\t● Which operating system are you using?\n\t● If you encountered an issue with failed transactions please consider switching to a new data directory.\n\t Sometimes the data directory gets corrupted and leads to strange bugs. \n\t See: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nPlease make yourself familiar with the basic rules for the dispute process:\n\t● You need to respond to the {0}'s requests within 2 days.\n\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\t● The maximum period for a dispute is 14 days.\n\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\nYou can read more about the dispute process at: {2} support.systemMsg=Tin nhắn hệ thống: {0} support.youOpenedTicket=Bạn đã mở yêu cầu hỗ trợ.\n\n{0}\n\nPhiên bản Haveno: {1} support.youOpenedDispute=Bạn đã mở yêu cầu giải quyết tranh chấp.\n\n{0}\n\nPhiên bản Haveno: {1} @@ -989,8 +987,8 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nHaveno v support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nHaveno version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nHaveno version: {1} -support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} -support.mediatorsAddress=Mediator''s node address: {0} +support.mediatorsDisputeSummary=System message: Mediator's dispute summary:\n{0} +support.mediatorsAddress=Mediator's node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. Please inform the developers about that incident and do not close that case before the situation is resolved!\n\nAddress used in the dispute: {0}\n\nAll DAO param donation addresses: {1}\n\nTrade ID: {2}{3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. @@ -1033,7 +1031,8 @@ setting.preferences.addCrypto=Bổ sung crypto setting.preferences.displayOptions=Hiển thị các phương án setting.preferences.showOwnOffers=Hiển thị Báo giá của tôi trong danh mục Báo giá setting.preferences.useAnimations=Sử dụng hoạt ảnh -setting.preferences.useDarkMode=Use dark mode +setting.preferences.useDarkMode=Sử dụng chế độ tối +setting.preferences.useLightMode=Sử dụng chế độ sáng setting.preferences.sortWithNumOffers=Sắp xếp danh sách thị trường với số chào giá/giao dịch setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1049,6 +1048,9 @@ settings.preferences.editCustomExplorer.name=Tên settings.preferences.editCustomExplorer.txUrl=Transaction URL settings.preferences.editCustomExplorer.addressUrl=Address URL +setting.info.headline=Tính năng bảo mật dữ liệu mới +settings.preferences.sensitiveDataRemoval.msg=Để bảo vệ quyền riêng tư của bạn và các nhà giao dịch khác, Haveno dự định sẽ xóa dữ liệu nhạy cảm khỏi các giao dịch cũ. Điều này đặc biệt quan trọng đối với các giao dịch tiền pháp định có thể bao gồm thông tin tài khoản ngân hàng.\n\nBạn nên đặt giá trị này càng thấp càng tốt, ví dụ 60 ngày. Điều đó có nghĩa là các giao dịch đã hoàn thành từ hơn 60 ngày trước sẽ bị xóa dữ liệu nhạy cảm. Các giao dịch đã hoàn thành có thể được tìm thấy trong tab Danh mục đầu tư / Lịch sử. + settings.net.xmrHeader=Mạng Monero settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Địa chỉ onion của tôi @@ -1115,19 +1117,19 @@ setting.about.subsystems.label=Các phiên bản của hệ thống con setting.about.subsystems.val=Phiên bản mạng: {0}; Phiên bản tin nhắn P2P: {1}; Phiên bản DB nội bộ: {2}; Phiên bản giao thức giao dịch: {3} setting.about.shortcuts=Short cuts -setting.about.shortcuts.ctrlOrAltOrCmd=''Ctrl + {0}'' or ''alt + {0}'' or ''cmd + {0}'' +setting.about.shortcuts.ctrlOrAltOrCmd='Ctrl + {0}' or 'alt + {0}' or 'cmd + {0}' setting.about.shortcuts.menuNav=Navigate main menu setting.about.shortcuts.menuNav.value=To navigate the main menu press: 'Ctrl' or 'alt' or 'cmd' with a numeric key between '1-9' setting.about.shortcuts.close=Close Haveno -setting.about.shortcuts.close.value=''Ctrl + {0}'' or ''cmd + {0}'' or ''Ctrl + {1}'' or ''cmd + {1}'' +setting.about.shortcuts.close.value='Ctrl + {0}' or 'cmd + {0}' or 'Ctrl + {1}' or 'cmd + {1}' setting.about.shortcuts.closePopup=Close popup or dialog window setting.about.shortcuts.closePopup.value='ESCAPE' key setting.about.shortcuts.chatSendMsg=Send trader chat message -setting.about.shortcuts.chatSendMsg.value=''Ctrl + ENTER'' or ''alt + ENTER'' or ''cmd + ENTER'' +setting.about.shortcuts.chatSendMsg.value='Ctrl + ENTER' or 'alt + ENTER' or 'cmd + ENTER' setting.about.shortcuts.openDispute=Open dispute setting.about.shortcuts.openDispute.value=Select pending trade and click: {0} @@ -1202,7 +1204,7 @@ account.arbitratorRegistration.registerSuccess=You have successfully registered account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.crypto.yourCryptoAccounts=Tài khoản crypto của bạn -account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or (b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. +account.crypto.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don't control your keys or (b) which don't use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is not a {2} specialist and cannot help in such cases. account.crypto.popup.wallet.confirm=Tôi hiểu và xác nhận rằng tôi đã biết loại ví mình cần sử dụng. # suppress inspection "UnusedProperty" account.crypto.popup.upx.msg=Trading UPX on Haveno requires that you understand and fulfill the following requirements:\n\nFor sending UPX, you need to use either the official uPlexa GUI wallet or uPlexa CLI wallet with the store-tx-info flag enabled (default in new versions). Please be sure you can access the tx key as that would be required in case of a dispute.\nuplexa-wallet-cli (use the command get_tx_key)\nuplexa-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\nAt normal block explorers the transfer is not verifiable.\n\nYou need to provide the arbitrator the following data in case of a dispute:\n- The tx private key\n- The transaction hash\n- The recipient's public address\n\nFailure to provide the above data, or if you used an incompatible wallet, will result in losing the dispute case. The UPX sender is responsible for providing verification of the UPX transfer to the arbitrator in case of a dispute.\n\nThere is no payment ID required, just the normal public address.\nIf you are not sure about that process visit uPlexa discord channel (https://discord.gg/vhdNSrV) or the uPlexa Telegram Chat (https://t.me/uplexaOfficial) to find more information. @@ -1318,7 +1320,7 @@ account.notifications.marketAlert.manageAlerts.header.offerType=Loại chào gi account.notifications.marketAlert.message.title=Thông báo chào giá account.notifications.marketAlert.message.msg.below=cao hơn account.notifications.marketAlert.message.msg.above=thấp hơn -account.notifications.marketAlert.message.msg=một ''{0} {1}'' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán ''{5}''đã được đăng lên danh mục chào giá của Haveno.\nMã chào giá: {6}. +account.notifications.marketAlert.message.msg=một '{0} {1}' chào giá mới với giá {2} ({3} {4} giá thị trường) và hình thức thanh toán '{5}'đã được đăng lên danh mục chào giá của Haveno.\nMã chào giá: {6}. account.notifications.priceAlert.message.title=Thông báo giá cho {0} account.notifications.priceAlert.message.msg=Thông báo giá của bạn đã được kích hoạt. Giá {0} hiện tại là {1} {2} account.notifications.noWebCamFound.warning=Không tìm thấy webcam.\n\nVui lòng sử dụng lựa chọn email để gửi mã bảo mật và khóa mã hóa từ điện thoại di động của bạn tới ứng dùng Haveno. @@ -1471,11 +1473,14 @@ offerDetailsWindow.countryBank=Quốc gia ngân hàng của người tạo offerDetailsWindow.commitment=Cam kết offerDetailsWindow.agree=Tôi đồng ý offerDetailsWindow.tac=Điều khoản và điều kiện -offerDetailsWindow.confirm.maker=Xác nhận: Đặt chào giá cho {0} monero -offerDetailsWindow.confirm.taker=Xác nhận: Nhận chào giáo cho {0} monero +offerDetailsWindow.confirm.maker.buy=Xác nhận: Tạo đề nghị mua XMR bằng {0} +offerDetailsWindow.confirm.maker.sell=Xác nhận: Tạo đề nghị bán XMR lấy {0} +offerDetailsWindow.confirm.taker.buy=Xác nhận: Chấp nhận đề nghị mua XMR bằng {0} +offerDetailsWindow.confirm.taker.sell=Xác nhận: Chấp nhận đề nghị bán XMR lấy {0} offerDetailsWindow.creationDate=Ngày tạo offerDetailsWindow.makersOnion=Địa chỉ onion của người tạo offerDetailsWindow.challenge=Mã bảo vệ giao dịch +offerDetailsWindow.challenge.copy=Sao chép cụm mật khẩu để chia sẻ với đối tác của bạn qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1561,7 +1566,7 @@ torNetworkSettingWindow.deleteFiles.button=Xóa các file Tor đã hết hạn v torNetworkSettingWindow.deleteFiles.progress=Đang tắt Tor torNetworkSettingWindow.deleteFiles.success=Các file Tor lỗi thời đã xóa thành công. Vui lòng khởi động lại. torNetworkSettingWindow.bridges.header=Tor bị khoá? -torNetworkSettingWindow.bridges.info=Nếu Tor bị kẹt do nhà cung cấp internet hoặc quốc gia của bạn, bạn có thể thử dùng cầu nối Tor.\nTruy cập trang web Tor tại: https://bridges.torproject.org/bridges để biết thêm về cầu nối và phương tiện vận chuyển kết nối được. +torNetworkSettingWindow.bridges.info=Nếu Tor bị kẹt do nhà cung cấp internet hoặc quốc gia của bạn, bạn có thể thử dùng cầu nối Tor.\nTruy cập trang web Tor tại: https://bridges.torproject.org để biết thêm về cầu nối và phương tiện vận chuyển kết nối được. feeOptionWindow.headline=Chọn đồng tiền để thanh toán phí giao dịch feeOptionWindow.info=Bạn có thể chọn thanh toán phí giao dịch bằng BSQ hoặc XMR. Nếu bạn chọn BSQ, bạn sẽ được khấu trừ phí giao dịch. @@ -1682,8 +1687,8 @@ popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey popup.accountSigning.success.headline=Congratulations popup.accountSigning.success.description=All {0} payment accounts were successfully signed! popup.accountSigning.generalInformation=You'll find the signing state of all your accounts in the account section.\n\nFor further information, please visit [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. -popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer''s account after a successful trade.\n\n{0} -popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you''ll be able to sign other accounts in {0} days from now.\n\n{1} +popup.accountSigning.signedByArbitrator=One of your payment accounts has been verified and signed by an arbitrator. Trading with this account will automatically sign your trading peer's account after a successful trade.\n\n{0} +popup.accountSigning.signedByPeer=One of your payment accounts has been verified and signed by a trading peer. Your initial trading limit will be lifted and you'll be able to sign other accounts in {0} days from now.\n\n{1} popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts has been lifted.\n\n{0} popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts and the initial limit for one of your accounts has been lifted.\n\n{0} @@ -1978,8 +1983,8 @@ payment.accountType=Loại tài khoản payment.checking=Đang kiểm tra payment.savings=Tiết kiệm payment.personalId=ID cá nhân -payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle''s somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. -payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver''s full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account''s age and signing status are preserved. +payment.zelle.info=Zelle is a money transfer service that works best *through* another bank.\n\n1. Check this page to see if (and how) your bank works with Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Take special note of your transfer limits—sending limits vary by bank, and banks often specify separate daily, weekly, and monthly limits.\n\n3. If your bank does not work with Zelle, you can still use it through the Zelle mobile app, but your transfer limits will be much lower.\n\n4. The name specified on your Haveno account MUST match the name on your Zelle/bank account. \n\nIf you cannot complete a Zelle transaction as specified in your trade contract, you may lose some (or all) of your security deposit.\n\nBecause of Zelle's somewhat higher chargeback risk, sellers are advised to contact unsigned buyers through email or SMS to verify that the buyer really owns the Zelle account specified in Haveno. +payment.fasterPayments.newRequirements.info=Some banks have started verifying the receiver's full name for Faster Payments transfers. Your current Faster Payments account does not specify a full name.\n\nPlease consider recreating your Faster Payments account in Haveno to provide future {0} buyers with a full name.\n\nWhen you recreate the account, make sure to copy the precise sort code, account number and account age verification salt values from your old account to your new account. This will ensure your existing account's age and signing status are preserved. payment.moneyGram.info=When using MoneyGram the XMR buyer has to send the Authorisation number and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, country, state and the amount. The seller's email will be displayed to the buyer during the trade process. payment.westernUnion.info=When using Western Union the XMR buyer has to send the MTCN (tracking number) and a photo of the receipt by email to the XMR seller. The receipt must clearly show the seller's full name, city, country and the amount. The seller's email will be displayed to the buyer during the trade process. payment.halCash.info=Khi sử dụng HalCash người mua XMR cần phải gửi cho người bán XMR mã HalCash bằng tin nhắn điện thoại.\n\nVui lòng đảm bảo là lượng tiền này không vượt quá số lượng tối đa mà ngân hàng của bạn cho phép gửi khi dùng HalCash. Số lượng rút tối thiểu là 10 EUR và tối đa là 600 EUR. Nếu rút nhiều lần thì giới hạn sẽ là 3000 EUR/ người nhận/ ngày và 6000 EUR/người nhận/tháng. Vui lòng kiểm tra chéo những giới hạn này với ngân hàng của bạn để chắc chắn là họ cũng dùng những giới hạn như ghi ở đây.\n\nSố tiền rút phải là bội số của 10 EUR vì bạn không thể rút các mệnh giá khác từ ATM. Giao diện người dùng ở phần 'tạo chào giá' và 'chấp nhận chào giá' sẽ điều chỉnh lượng btc sao cho lượng EUR tương ứng sẽ chính xác. Bạn không thể dùng giá thị trường vì lượng EUR có thể sẽ thay đổi khi giá thay đổi.\n\nTrường hợp tranh chấp, người mua XMR cần phải cung cấp bằng chứng chứng minh mình đã gửi EUR. @@ -1991,7 +1996,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade payment.cashDeposit.info=Vui lòng xác nhận rằng ngân hàng của bạn cho phép nạp tiền mặt vào tài khoản của người khác. Chẳng hạn, Ngân Hàng Mỹ và Wells Fargo không còn cho phép nạp tiền như vậy nữa. payment.revolut.info=Revolut requires the 'Username' as account ID not the phone number or email as it was the case in the past. -payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a ''Username''.\nPlease enter your Revolut ''Username'' to update your account data.\nThis will not affect your account age signing status. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not have a 'Username'.\nPlease enter your Revolut 'Username' to update your account data.\nThis will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.cashapp.info=Vui lòng lưu ý rằng Cash App có rủi ro bồi hoàn cao hơn so với hầu hết các chuyển khoản ngân hàng. @@ -2025,7 +2030,7 @@ payment.japan.recipient=Tên payment.australia.payid=PayID payment.payid=PayID linked to financial institution. Like email address or mobile phone. payment.payid.info=A PayID like a phone number, email address or an Australian Business Number (ABN), that you can securely link to your bank, credit union or building society account. You need to have already created a PayID with your Australian financial institution. Both sending and receiving financial institutions must support PayID. For more information please check [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=Vì sự bảo vệ của bạn, chúng tôi khuyến cáo không nên sử dụng mã PIN Paysafecard để thanh toán.\n\n\ Các giao dịch được thực hiện bằng mã PIN không thể được xác minh độc lập để giải quyết tranh chấp. Nếu có vấn đề xảy ra, có thể không thể khôi phục số tiền đã mất.\n\n\ Để đảm bảo an toàn giao dịch và có thể giải quyết tranh chấp, hãy luôn sử dụng các phương thức thanh toán có hồ sơ xác minh được. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index f1b086eec3..db813c05aa 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=复审:报价挂单 {0} 比特币 +createOffer.placeOfferButton.buy=审核:创建以 {0} 买入 XMR 的报价 +createOffer.placeOfferButton.sell=审核:创建以 {0} 卖出 XMR 的报价 createOffer.createOfferFundWalletInfo.headline=为您的报价充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n @@ -525,7 +521,8 @@ takeOffer.success.info=你可以在“业务/未完成交易”页面内查看 takeOffer.error.message=下单时发生了一个错误。\n\n{0} # new entries -takeOffer.takeOfferButton=复审:报价下单 {0} 比特币 +takeOffer.takeOfferButton.buy=审核:接受以 {0} 买入 XMR 的报价 +takeOffer.takeOfferButton.sell=审核:接受以 {0} 卖出 XMR 的报价 takeOffer.noPriceFeedAvailable=您不能对这笔报价下单,因为它使用交易所价格百分比定价,但是您没有获得可用的价格。 takeOffer.takeOfferFundWalletInfo.headline=为交易充值 # suppress inspection "TrailingSpacesInProperty" @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=与市场价格偏差百分比 portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=等待区块链确认 +portfolio.pending.step2_buyer.additionalConf=存款已达到 10 个确认。\n为了额外安全,我们建议在发送付款前等待 {0} 个确认。\n提前操作风险自负。 portfolio.pending.step2_buyer.startPayment=开始付款 portfolio.pending.step2_seller.waitPaymentSent=等待直到付款 portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到达 @@ -644,7 +642,7 @@ portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=请通过提供的联系人与 XMR 卖家联系,并安排会议支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 开始付款 @@ -795,6 +793,8 @@ portfolio.pending.support.text.getHelp=如果您有任何问题,您可以尝 portfolio.pending.support.button.getHelp=开启交易聊天 portfolio.pending.support.headline.halfPeriodOver=确认付款 portfolio.pending.support.headline.periodOver=交易期结束 +portfolio.pending.support.headline.depositTxMissing=缺少存款交易 +portfolio.pending.support.depositTxMissing=此交易缺少存款交易。请提交支持工单以联系仲裁员寻求帮助。 portfolio.pending.mediationRequested=已请求调解员协助 portfolio.pending.refundRequested=已请求退款 @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已经接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃单交易费未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=挂单费交易未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 -portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/haveno-dex/haveno/issues\n\n请随意的将该交易移至失败交易 +portfolio.pending.failedTrade.missingDepositTx=缺少一笔保证金交易。\n\n该交易是完成交易所必需的。请确保您的钱包已与 Monero 区块链完全同步。\n\n您可以将此交易移动到“失败的交易”部分以将其停用。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues @@ -927,8 +927,6 @@ support.tab.mediation.support=调解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=历史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工单 -support.filter=查找纠纷 -support.filter.prompt=输入 交易 ID、日期、洋葱地址或账户信息 support.sigCheck.button=Check signature support.sigCheck.popup.info=请粘贴仲裁过程的摘要信息。使用这个工具,任何用户都可以检查仲裁者的签名是否与摘要信息相符。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=显示选项 setting.preferences.showOwnOffers=在报价列表中显示我的报价 setting.preferences.useAnimations=使用动画 setting.preferences.useDarkMode=使用夜间模式 +setting.preferences.useLightMode=使用浅色模式 setting.preferences.sortWithNumOffers=使用“报价ID/交易ID”筛选列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1050,6 +1049,9 @@ settings.preferences.editCustomExplorer.name=名称 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL +setting.info.headline=新的数据隐私功能 +settings.preferences.sensitiveDataRemoval.msg=为了保护您和其他交易者的隐私,Haveno 计划从旧交易中删除敏感数据。这对于可能包含银行账户信息的法币交易尤其重要。\n\n建议将其设置得尽可能低,例如 60 天。这意味着超过 60 天且已完成的交易将被清除敏感数据。已完成的交易可在“投资组合 / 历史”标签中找到。 + settings.net.xmrHeader=比特币网络 settings.net.p2pHeader=Haveno 网络 settings.net.onionAddressLabel=我的匿名地址 @@ -1161,7 +1163,7 @@ setting.about.shortcuts.sendPrivateNotification=发送私人通知到对等点 setting.about.shortcuts.sendPrivateNotification.value=点击交易伙伴头像并按下:{0} 以显示更多信息 setting.info.headline=新 XMR 自动确认功能 -setting.info.msg=当你完成 XMR/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Haveno 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Haveno 使用由 Haveno 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置''中将每笔交易的最大 XMR 数量设置为自动确认以及所需确认的数量。\n\n在 Haveno Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades +setting.info.msg=当你完成 XMR/XMR 交易时,您可以使用自动确认功能来验证是否向您的钱包中发送了正确数量的 XMR,以便 Haveno 可以自动将交易标记为完成,从而使每个人都可以更快地进行交易。\n\n自动确认使用 XMR 发送方提供的交易密钥在至少 2 个 XMR 区块浏览器节点上检查 XMR 交易。在默认情况下,Haveno 使用由 Haveno 贡献者运行的区块浏览器节点,但是我们建议运行您自己的 XMR 区块浏览器节点以最大程度地保护隐私和安全。\n\n您还可以在``设置'中将每笔交易的最大 XMR 数量设置为自动确认以及所需确认的数量。\n\n在 Haveno Wiki 上查看更多详细信息(包括如何设置自己的区块浏览器节点):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### @@ -1473,11 +1475,14 @@ offerDetailsWindow.countryBank=卖家银行所在国家或地区 offerDetailsWindow.commitment=承诺 offerDetailsWindow.agree=我同意 offerDetailsWindow.tac=条款和条件 -offerDetailsWindow.confirm.maker=确定:发布报价 {0} 比特币 -offerDetailsWindow.confirm.taker=确定:下单买入 {0} 比特币 +offerDetailsWindow.confirm.maker.buy=确认:创建以 {0} 买入 XMR 的报价 +offerDetailsWindow.confirm.maker.sell=确认:创建以 {0} 卖出 XMR 的报价 +offerDetailsWindow.confirm.taker.buy=确认:接受以 {0} 买入 XMR 的报价 +offerDetailsWindow.confirm.taker.sell=确认:接受以 {0} 卖出 XMR 的报价 offerDetailsWindow.creationDate=创建时间 offerDetailsWindow.makersOnion=卖家的匿名地址 offerDetailsWindow.challenge=提供密码 +offerDetailsWindow.challenge.copy=复制助记词以与您的交易对手共享 qRCodeWindow.headline=二维码 qRCodeWindow.msg=请使用二维码从外部钱包充值至 Haveno 钱包 @@ -1564,7 +1569,7 @@ torNetworkSettingWindow.deleteFiles.button=删除过期的 Tor 文件并关闭 torNetworkSettingWindow.deleteFiles.progress=关闭正在运行中的 Tor torNetworkSettingWindow.deleteFiles.success=过期的 Tor 文件被成功删除。请重新启动。 torNetworkSettingWindow.bridges.header=Tor 网络被屏蔽? -torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的国家或地区屏蔽,您可以尝试使用 Tor 网桥。\n \n访问 Tor 网页:https://bridges.torproject.org/bridges,了解关于网桥和可插拔传输的更多信息。 +torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的国家或地区屏蔽,您可以尝试使用 Tor 网桥。\n \n访问 Tor 网页:https://bridges.torproject.org,了解关于网桥和可插拔传输的更多信息。 feeOptionWindow.headline=选择货币支付交易手续费 feeOptionWindow.info=您可以选择用 BSQ 或 XMR 支付交易费用。如果您选择 BSQ ,您会感谢这些交易手续费折扣。 @@ -1603,7 +1608,7 @@ error.closedTradeWithUnconfirmedDepositTx=交易 ID 为 {0} 的已关闭交易 error.closedTradeWithNoDepositTx=交易 ID 为 {0} 的保证金交易已被确认。\n\n请重新启动应用程序来清理已关闭的交易列表。 popup.warning.walletNotInitialized=钱包至今未初始化 -popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Haveno 使用Java)会在 MacOS 中引发弹出警告(``Haveno 希望从任何应用程序接收击键'').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Haveno”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno将升级到新的 Java 版本,以避免该问题。 +popup.warning.osxKeyLoggerWarning=由于 MacOS 10.14 及更高版本中的安全措施更加严格,因此启动 Java 应用程序(Haveno 使用Java)会在 MacOS 中引发弹出警告(``Haveno 希望从任何应用程序接收击键').\n\n为了避免该问题,请打开“ MacOS 设置”,然后转到“安全和隐私”->“隐私”->“输入监视”,然后从右侧列表中删除“ Haveno”。\n\n一旦解决了技术限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno将升级到新的 Java 版本,以避免该问题。 popup.warning.wrongVersion=您这台电脑上可能有错误的 Haveno 版本。\n您的电脑的架构是:{0}\n您安装的 Haveno 二进制文件是:{1}\n请关闭并重新安装正确的版本({2})。 popup.warning.incompatibleDB=我们检测到不兼容的数据库文件!\n\n那些数据库文件与我们当前的代码库不兼容:\n{0}\n\n我们对损坏的文件进行了备份,并将默认值应用于新的数据库版本。\n\n备份位于:\n{1}/db/backup_of_corrupted_data。\n\n请检查您是否安装了最新版本的 Haveno\n您可以下载:\nhttps://haveno.exchange/downloads\n\n请重新启动应用程序。 popup.warning.startupFailed.twoInstances=Haveno 已经在运行。 您不能运行两个 Haveno 实例。 @@ -1626,6 +1631,7 @@ popup.warning.btcChangeBelowDustException=该交易创建的更改输出低于 popup.warning.messageTooLong=您的信息超过最大允许的大小。请将其分成多个部分发送,或将其上传到 https://pastebin.com 之类的服务器。 popup.warning.lockedUpFunds=你已经从一个失败的交易中冻结了资金。\n冻结余额:{0}\n存款tx地址:{1}\n交易单号:{2}\n\n请通过选择待处理交易界面中的交易并点击“alt + o”或“option+ o”打开帮助话题。 +popup.warning.moneroConnection=连接 Monero 网络时出现问题。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1998,7 +2004,7 @@ payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个 payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不是像以往的电话号码或电子邮件。 -payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名''以更新您的帐户数据。\n这不会影响您的账龄验证状态。 +payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名'以更新您的帐户数据。\n这不会影响您的账龄验证状态。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 payment.cashapp.info=请注意,Cash App 的退款风险高于大多数银行转账。 @@ -2035,7 +2041,7 @@ payment.japan.recipient=名称 payment.australia.payid=PayID payment.payid=PayID 需链接至金融机构。例如电子邮件地址或手机。 payment.payid.info=PayID,如电话号码、电子邮件地址或澳大利亚商业号码(ABN),您可以安全地连接到您的银行、信用合作社或建立社会帐户。你需要在你的澳大利亚金融机构创建一个 PayID。发送和接收金融机构都必须支持 PayID。更多信息请查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=为了保障您的安全,我们强烈不建议使用 Paysafecard PIN 进行支付。\n\n\ 通过 PIN 进行的交易无法被独立验证以解决争议。如果出现问题,资金可能无法追回。\n\n\ 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 94554bc12f..e3f943d80c 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -4,11 +4,6 @@ # E.g.: [main-view].[component].[description] # In some cases we use enum values or constants to map to display strings -# A annoying issue with property files is that we need to use 2 single quotes in display string -# containing variables (e.g. {0}), otherwise the variable will not be resolved. -# In display string which do not use a variable a single quote is ok. -# E.g. Don''t .... {1} - # We use sometimes dynamic parts which are put together in the code and therefore sometimes use line breaks or spaces # at the end of the string. Please never remove any line breaks or spaces. They are there with a purpose! # To make longer strings with better readable you can make a line break with \ which does not result in a line break @@ -218,7 +213,7 @@ shared.delayedPayoutTxId=延遲支付交易 ID shared.delayedPayoutTxReceiverAddress=延遲交易交易已發送至 shared.unconfirmedTransactionsLimitReached=你現在有過多的未確認交易。請稍後嘗試 shared.numItemsLabel=Number of entries: {0} -shared.filter=Filter +shared.filter=篩選 shared.enabled=啟用 @@ -460,7 +455,8 @@ createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries -createOffer.placeOfferButton=複審:報價掛單 {0} 比特幣 +createOffer.placeOfferButton.buy=審核:建立以 {0} 買入 XMR 的報價 +createOffer.placeOfferButton.sell=審核:建立以 {0} 賣出 XMR 的報價 createOffer.createOfferFundWalletInfo.headline=為您的報價充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n @@ -525,7 +521,8 @@ takeOffer.success.info=你可以在“業務/未完成交易”頁面內查看 takeOffer.error.message=下單時發生了一個錯誤。\n\n{0} # new entries -takeOffer.takeOfferButton=複審:報價下單 {0} 比特幣 +takeOffer.takeOfferButton.buy=審核:接受以 {0} 買入 XMR 的報價 +takeOffer.takeOfferButton.sell=審核:接受以 {0} 賣出 XMR 的報價 takeOffer.noPriceFeedAvailable=您不能對這筆報價下單,因為它使用交易所價格百分比定價,但是您沒有獲得可用的價格。 takeOffer.takeOfferFundWalletInfo.headline=為交易充值 # suppress inspection "TrailingSpacesInProperty" @@ -580,6 +577,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0} portfolio.pending.step1.waitForConf=等待區塊鏈確認 +portfolio.pending.step2_buyer.additionalConf=存款已達 10 次確認。\n為了額外安全,我們建議在發送付款前等待 {0} 次確認。\n提前操作風險自負。 portfolio.pending.step2_buyer.startPayment=開始付款 portfolio.pending.step2_seller.waitPaymentSent=等待直到付款 portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到達 @@ -644,7 +642,7 @@ portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Please send {0} using \"Pay by Mail\" to the XMR seller. Specific instructions are in the trade contract, or if unclear you may ask questions via trader chat. See more details about Pay by Mail on the Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You''ll find the seller's account details on the next screen.\n\n +portfolio.pending.step2_buyer.pay=Please pay {0} via the specified payment method to the XMR seller. You'll find the seller's account details on the next screen.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=請通過提供的聯繫人與 XMR 賣家聯繫,並安排會議支付 {0}。\n\n portfolio.pending.step2_buyer.startPaymentUsing=使用 {0} 開始付款 @@ -795,6 +793,8 @@ portfolio.pending.support.text.getHelp=如果您有任何問題,您可以嘗 portfolio.pending.support.button.getHelp=開啟交易聊天 portfolio.pending.support.headline.halfPeriodOver=確認付款 portfolio.pending.support.headline.periodOver=交易期結束 +portfolio.pending.support.headline.depositTxMissing=缺少存款交易 +portfolio.pending.support.depositTxMissing=此交易缺少存款。請開啟支援工單以聯絡仲裁者協助處理。 portfolio.pending.mediationRequested=已請求調解員協助 portfolio.pending.refundRequested=已請求退款 @@ -820,7 +820,7 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已經接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃單交易費未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=掛單費交易未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 -portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/haveno-dex/haveno/issues\n\n請隨意的將該交易移至失敗交易 +portfolio.pending.failedTrade.missingDepositTx=缺少一筆保證金交易。\n\n此交易是完成交易所必需的。請確保您的錢包已與 Monero 區塊鏈完全同步。\n\n您可以將此交易移至「失敗的交易」區段以停用它。 portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues @@ -927,8 +927,6 @@ support.tab.mediation.support=調解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=歷史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工單 -support.filter=查找糾紛 -support.filter.prompt=輸入 交易 ID、日期、洋葱地址或賬户信息 support.sigCheck.button=Check signature support.sigCheck.popup.info=請貼上仲裁程序的摘要訊息。利用這個工具,任何使用者都可以檢查仲裁者的簽名是否與摘要訊息相符。 @@ -1035,6 +1033,7 @@ setting.preferences.displayOptions=顯示選項 setting.preferences.showOwnOffers=在報價列表中顯示我的報價 setting.preferences.useAnimations=使用動畫 setting.preferences.useDarkMode=使用夜間模式 +setting.preferences.useLightMode=使用淺色模式 setting.preferences.sortWithNumOffers=使用“報價ID/交易ID”篩選列表 setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods setting.preferences.denyApiTaker=Deny takers using the API @@ -1050,6 +1049,9 @@ settings.preferences.editCustomExplorer.name=名稱 settings.preferences.editCustomExplorer.txUrl=交易 URL settings.preferences.editCustomExplorer.addressUrl=地址 URL +setting.info.headline=新的資料隱私功能 +settings.preferences.sensitiveDataRemoval.msg=為了保護您與其他交易者的隱私,Haveno 計劃從舊交易中移除敏感資料。這對於可能包含銀行帳戶資訊的法幣交易尤其重要。\n\n建議將此設定設為盡可能低,例如 60 天。這表示只要交易已完成且超過 60 天,敏感資料將被清除。已完成的交易可在「投資組合 / 歷史」標籤中找到。 + settings.net.xmrHeader=比特幣網絡 settings.net.p2pHeader=Haveno 網絡 settings.net.onionAddressLabel=我的匿名地址 @@ -1161,7 +1163,7 @@ setting.about.shortcuts.sendPrivateNotification=發送私人通知到對等點 setting.about.shortcuts.sendPrivateNotification.value=點擊交易夥伴頭像並按下:{0} 以顯示更多信息 setting.info.headline=新 XMR 自動確認功能 -setting.info.msg=當你完成 XMR/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Haveno 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Haveno 使用由 Haveno 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置''中將每筆交易的最大 XMR 數量設置為自動確認以及所需確認的數量。\n\n在 Haveno Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades +setting.info.msg=當你完成 XMR/XMR 交易時,您可以使用自動確認功能來驗證是否向您的錢包中發送了正確數量的 XMR,以便 Haveno 可以自動將交易標記為完成,從而使每個人都可以更快地進行交易。\n\n自動確認使用 XMR 發送方提供的交易密鑰在至少 2 個 XMR 區塊瀏覽器節點上檢查 XMR 交易。在默認情況下,Haveno 使用由 Haveno 貢獻者運行的區塊瀏覽器節點,但是我們建議運行您自己的 XMR 區塊瀏覽器節點以最大程度地保護隱私和安全。\n\n您還可以在``設置'中將每筆交易的最大 XMR 數量設置為自動確認以及所需確認的數量。\n\n在 Haveno Wiki 上查看更多詳細信息(包括如何設置自己的區塊瀏覽器節點):https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades #################################################################### # Account #################################################################### @@ -1473,11 +1475,14 @@ offerDetailsWindow.countryBank=賣家銀行所在國家或地區 offerDetailsWindow.commitment=承諾 offerDetailsWindow.agree=我同意 offerDetailsWindow.tac=條款和條件 -offerDetailsWindow.confirm.maker=確定:發佈報價 {0} 比特幣 -offerDetailsWindow.confirm.taker=確定:下單買入 {0} 比特幣 +offerDetailsWindow.confirm.maker.buy=確認:建立以 {0} 買入 XMR 的報價 +offerDetailsWindow.confirm.maker.sell=確認:建立以 {0} 賣出 XMR 的報價 +offerDetailsWindow.confirm.taker.buy=確認:接受以 {0} 買入 XMR 的報價 +offerDetailsWindow.confirm.taker.sell=確認:接受以 {0} 賣出 XMR 的報價 offerDetailsWindow.creationDate=創建時間 offerDetailsWindow.makersOnion=賣家的匿名地址 offerDetailsWindow.challenge=提供密碼 +offerDetailsWindow.challenge.copy=複製密語以與對方分享 qRCodeWindow.headline=二維碼 qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Haveno 錢包 @@ -1563,7 +1568,7 @@ torNetworkSettingWindow.deleteFiles.button=刪除過期的 Tor 文件並關閉 torNetworkSettingWindow.deleteFiles.progress=關閉正在運行中的 Tor torNetworkSettingWindow.deleteFiles.success=過期的 Tor 文件被成功刪除。請重新啟動。 torNetworkSettingWindow.bridges.header=Tor 網絡被屏蔽? -torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的國家或地區屏蔽,您可以嘗試使用 Tor 網橋。\n \n訪問 Tor 網頁:https://bridges.torproject.org/bridges,瞭解關於網橋和可插拔傳輸的更多信息。 +torNetworkSettingWindow.bridges.info=如果 Tor 被您的 Internet 提供商或您的國家或地區屏蔽,您可以嘗試使用 Tor 網橋。\n \n訪問 Tor 網頁:https://bridges.torproject.org,瞭解關於網橋和可插拔傳輸的更多信息。 feeOptionWindow.headline=選擇貨幣支付交易手續費 feeOptionWindow.optionsLabel=選擇貨幣支付交易手續費 @@ -1601,7 +1606,7 @@ error.closedTradeWithUnconfirmedDepositTx=交易 ID 為 {0} 的已關閉交易 error.closedTradeWithNoDepositTx=交易 ID 為 {0} 的保證金交易已被確認。\n\n請重新啟動應用程序來清理已關閉的交易列表。 popup.warning.walletNotInitialized=錢包至今未初始化 -popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Haveno 使用Java)會在 MacOS 中引發彈出警吿(``Haveno 希望從任何應用程序接收擊鍵'').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Haveno”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno將升級到新的 Java 版本,以避免該問題。 +popup.warning.osxKeyLoggerWarning=由於 MacOS 10.14 及更高版本中的安全措施更加嚴格,因此啟動 Java 應用程序(Haveno 使用Java)會在 MacOS 中引發彈出警吿(``Haveno 希望從任何應用程序接收擊鍵').\n\n為了避免該問題,請打開“ MacOS 設置”,然後轉到“安全和隱私”->“隱私”->“輸入監視”,然後從右側列表中刪除“ Haveno”。\n\n一旦解決了技術限制(所需的 Java 版本的 Java 打包程序尚未交付),Haveno將升級到新的 Java 版本,以避免該問題。 popup.warning.wrongVersion=您這台電腦上可能有錯誤的 Haveno 版本。\n您的電腦的架構是:{0}\n您安裝的 Haveno 二進制文件是:{1}\n請關閉並重新安裝正確的版本({2})。 popup.warning.incompatibleDB=我們檢測到不兼容的數據庫文件!\n\n那些數據庫文件與我們當前的代碼庫不兼容:\n{0}\n\n我們對損壞的文件進行了備份,並將默認值應用於新的數據庫版本。\n\n備份位於:\n{1}/db/backup_of_corrupted_data。\n\n請檢查您是否安裝了最新版本的 Haveno\n您可以下載:\nhttps://haveno.exchange/downloads\n\n請重新啟動應用程序。 popup.warning.startupFailed.twoInstances=Haveno 已經在運行。 您不能運行兩個 Haveno 實例。 @@ -1622,6 +1627,7 @@ popup.warning.noPriceFeedAvailable=該貨幣沒有可用的價格。 你不能 popup.warning.sendMsgFailed=向您的交易對象發送消息失敗。\n請重試,如果繼續失敗報吿錯誤。 popup.warning.messageTooLong=您的信息超過最大允許的大小。請將其分成多個部分發送,或將其上傳到 https://pastebin.com 之類的服務器。 popup.warning.lockedUpFunds=你已經從一個失敗的交易中凍結了資金。\n凍結餘額:{0}\n存款tx地址:{1}\n交易單號:{2}\n\n請通過選擇待處理交易界面中的交易並點擊“alt + o”或“option+ o”打開幫助話題。 +popup.warning.moneroConnection=連接到 Monero 網路時發生問題。\n\n{0} popup.warning.makerTxInvalid=This offer is not valid. Please choose a different offer.\n\n takeOffer.cancelButton=Cancel take-offer @@ -1992,7 +1998,7 @@ payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個 payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不是像以往的電話號碼或電子郵件。 -payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名''以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 +payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名'以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 payment.cashapp.info=請注意,Cash App 的退款風險高於大多數銀行轉帳。 @@ -2029,7 +2035,7 @@ payment.japan.recipient=名稱 payment.australia.payid=PayID payment.payid=PayID 需鏈接至金融機構。例如電子郵件地址或手機。 payment.payid.info=PayID,如電話號碼、電子郵件地址或澳大利亞商業號碼(ABN),您可以安全地連接到您的銀行、信用合作社或建立社會帳户。你需要在你的澳大利亞金融機構創建一個 PayID。發送和接收金融機構都必須支持 PayID。更多信息請查看[HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller''s email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card''s message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card''s message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) +payment.amazonGiftCard.info=To pay with Amazon eGift Card, you will need to send an Amazon eGift Card to the XMR seller via your Amazon account. \n\nHaveno will show the XMR seller's email address or phone number where the gift card should be sent, and you must include the trade ID in the gift card's message field. Please see the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] for further details and best practices. \n\nThree important notes:\n- try to send gift cards with amounts of 100 USD or smaller, as Amazon is known to flag larger gift cards as fraudulent\n- try to use creative, believable text for the gift card's message (e.g., "Happy birthday Susan!") along with the trade ID (and use trader chat to tell your trading peer the reference text you picked so they can verify your payment)\n- Amazon eGift Cards can only be redeemed on the Amazon website they were purchased on (e.g., a gift card purchased on amazon.it can only be redeemed on amazon.it) payment.paysafe.info=為了保護您的安全,我們強烈不建議使用 Paysafecard PIN 進行付款。\n\n\ 透過 PIN 進行的交易無法獨立驗證以進行爭議解決。如果發生問題,可能無法追回資金。\n\n\ 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 diff --git a/core/src/test/java/haveno/core/monetary/PriceTest.java b/core/src/test/java/haveno/core/monetary/PriceTest.java index 49a32ac09d..37aa618caa 100644 --- a/core/src/test/java/haveno/core/monetary/PriceTest.java +++ b/core/src/test/java/haveno/core/monetary/PriceTest.java @@ -59,13 +59,13 @@ public class PriceTest { ); assertEquals( - "10000.2346 LTC/XMR", + "10000.2346 XMR/LTC", parse("LTC", "10000,23456789").toFriendlyString(), "Too many decimals should get rounded up properly." ); assertEquals( - "10000.2345 LTC/XMR", + "10000.2345 XMR/LTC", parse("LTC", "10000,23454999").toFriendlyString(), "Too many decimals should get rounded down properly." ); @@ -108,13 +108,13 @@ public class PriceTest { ); assertEquals( - "10000.2346 LTC/XMR", + "10000.2346 XMR/LTC", valueOf("LTC", 1000023456789L).toFriendlyString(), "Too many decimals should get rounded up properly." ); assertEquals( - "10000.2345 LTC/XMR", + "10000.2345 XMR/LTC", valueOf("LTC", 1000023454999L).toFriendlyString(), "Too many decimals should get rounded down properly." ); diff --git a/core/src/test/java/haveno/core/payment/ReceiptPredicatesTest.java b/core/src/test/java/haveno/core/payment/ReceiptPredicatesTest.java index 1a0243b1b8..34ad938dc1 100644 --- a/core/src/test/java/haveno/core/payment/ReceiptPredicatesTest.java +++ b/core/src/test/java/haveno/core/payment/ReceiptPredicatesTest.java @@ -34,7 +34,7 @@ public class ReceiptPredicatesTest { @Test public void testIsMatchingCurrency() { Offer offer = mock(Offer.class); - when(offer.getCurrencyCode()).thenReturn("USD"); + when(offer.getCounterCurrencyCode()).thenReturn("USD"); PaymentAccount account = mock(PaymentAccount.class); when(account.getTradeCurrencies()).thenReturn(Lists.newArrayList( diff --git a/core/src/test/java/haveno/core/trade/TradableListTest.java b/core/src/test/java/haveno/core/trade/TradableListTest.java index 7577d6365a..5fac7064a5 100644 --- a/core/src/test/java/haveno/core/trade/TradableListTest.java +++ b/core/src/test/java/haveno/core/trade/TradableListTest.java @@ -38,7 +38,7 @@ public class TradableListTest { // test adding an OpenOffer and convert toProto Offer offer = new Offer(offerPayload); - OpenOffer openOffer = new OpenOffer(offer, 0); + OpenOffer openOffer = new OpenOffer(offer, 0, false); openOfferTradableList.add(openOffer); message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); assertEquals(message.getMessageCase(), TRADABLE_LIST); diff --git a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java index eb8e2e124d..f6da1dfd4c 100644 --- a/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java +++ b/core/src/test/java/haveno/core/util/coin/CoinUtilTest.java @@ -76,10 +76,11 @@ public class CoinUtilTest { BigInteger result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + Restrictions.getMinTradeAmount(), + HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( - HavenoUtils.formatXmr(Restrictions.MIN_TRADE_AMOUNT, true), + HavenoUtils.formatXmr(Restrictions.getMinTradeAmount(), true), HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed should be adjusted to the smallest trade allowed." ); @@ -88,12 +89,13 @@ public class CoinUtilTest { CoinUtil.getAdjustedAmount( BigInteger.ZERO, Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.2), 1); fail("Expected IllegalArgumentException to be thrown when amount is too low."); } catch (IllegalArgumentException iae) { assertEquals( - "amount needs to be above minimum of 0.1 xmr", + "amount must be above minimum of 0.05 xmr but was 0.0 xmr", iae.getMessage(), "Unexpected exception message." ); @@ -102,10 +104,11 @@ public class CoinUtilTest { result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.2).longValueExact(), + Restrictions.getMinTradeAmount(), + HavenoUtils.xmrToAtomicUnits(0.2), 1); assertEquals( - "0.10 XMR", + "0.05 XMR", HavenoUtils.formatXmr(result, true), "Minimum allowed trade amount should not be adjusted." ); @@ -113,26 +116,23 @@ public class CoinUtilTest { result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.25).longValueExact(), + Restrictions.getMinTradeAmount(), + HavenoUtils.xmrToAtomicUnits(0.25), 1); assertEquals( - "0.10 XMR", + "0.05 XMR", HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed should respect maxTradeLimit and factor, if possible." ); - // TODO(chirhonul): The following seems like it should raise an exception or otherwise fail. - // We are asking for the smallest allowed BTC trade when price is 1000 USD each, and the - // max trade limit is 5k sat = 0.00005 BTC. But the returned amount 0.00005 BTC, or - // 0.05 USD worth, which is below the factor of 1 USD, but does respect the maxTradeLimit. - // Basically the given constraints (maxTradeLimit vs factor) are impossible to both fulfill.. result = CoinUtil.getAdjustedAmount( HavenoUtils.xmrToAtomicUnits(0.1), Price.valueOf("USD", 1000_0000), - HavenoUtils.xmrToAtomicUnits(0.00005).longValueExact(), + HavenoUtils.xmrToAtomicUnits(0.1), + HavenoUtils.xmrToAtomicUnits(0.5), 1); assertEquals( - "0.00005 XMR", + "0.10 XMR", HavenoUtils.formatXmr(result, true), "Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified." ); diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcPriceService.java index 223b1f2874..f867721560 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcPriceService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcPriceService.java @@ -48,7 +48,6 @@ import haveno.proto.grpc.MarketPriceReply; import haveno.proto.grpc.MarketPriceRequest; import haveno.proto.grpc.MarketPricesReply; import haveno.proto.grpc.MarketPricesRequest; -import static haveno.proto.grpc.PriceGrpc.PriceImplBase; import static haveno.proto.grpc.PriceGrpc.getGetMarketPriceMethod; import haveno.proto.grpc.PriceGrpc.PriceImplBase; import io.grpc.ServerInterceptor; @@ -57,8 +56,6 @@ import java.util.HashMap; import java.util.List; import java.util.Optional; -import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; -import static haveno.proto.grpc.PriceGrpc.getGetMarketPriceMethod; import static java.util.concurrent.TimeUnit.SECONDS; import lombok.extern.slf4j.Slf4j; diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java index 1de4580038..d2a5fc49b5 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java @@ -22,11 +22,15 @@ import com.google.inject.Singleton; import haveno.common.config.Config; import haveno.core.api.CoreContext; import haveno.daemon.grpc.interceptor.PasswordAuthInterceptor; -import io.grpc.Server; -import io.grpc.ServerBuilder; import static io.grpc.ServerInterceptors.interceptForward; import java.io.IOException; import java.io.UncheckedIOException; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Server; +import io.grpc.ServerBuilder; import lombok.extern.slf4j.Slf4j; @Singleton @@ -55,26 +59,41 @@ public class GrpcServer { GrpcXmrConnectionService moneroConnectionsService, GrpcXmrNodeService moneroNodeService) { this.server = ServerBuilder.forPort(config.apiPort) - .addService(interceptForward(accountService, accountService.interceptors())) - .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) - .addService(interceptForward(disputesService, disputesService.interceptors())) - .addService(interceptForward(helpService, helpService.interceptors())) - .addService(interceptForward(offersService, offersService.interceptors())) - .addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors())) - .addService(interceptForward(priceService, priceService.interceptors())) .addService(shutdownService) - .addService(interceptForward(tradeStatisticsService, tradeStatisticsService.interceptors())) - .addService(interceptForward(tradesService, tradesService.interceptors())) - .addService(interceptForward(versionService, versionService.interceptors())) - .addService(interceptForward(walletsService, walletsService.interceptors())) - .addService(interceptForward(notificationsService, notificationsService.interceptors())) - .addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors())) - .addService(interceptForward(moneroNodeService, moneroNodeService.interceptors())) .intercept(passwordAuthInterceptor) + .addService(interceptForward(accountService, config.disableRateLimits ? interceptors() : accountService.interceptors())) + .addService(interceptForward(disputeAgentsService, config.disableRateLimits ? interceptors() : disputeAgentsService.interceptors())) + .addService(interceptForward(disputesService, config.disableRateLimits ? interceptors() : disputesService.interceptors())) + .addService(interceptForward(helpService, config.disableRateLimits ? interceptors() : helpService.interceptors())) + .addService(interceptForward(offersService, config.disableRateLimits ? interceptors() : offersService.interceptors())) + .addService(interceptForward(paymentAccountsService, config.disableRateLimits ? interceptors() : paymentAccountsService.interceptors())) + .addService(interceptForward(priceService, config.disableRateLimits ? interceptors() : priceService.interceptors())) + .addService(interceptForward(tradeStatisticsService, config.disableRateLimits ? interceptors() : tradeStatisticsService.interceptors())) + .addService(interceptForward(tradesService, config.disableRateLimits ? interceptors() : tradesService.interceptors())) + .addService(interceptForward(versionService, config.disableRateLimits ? interceptors() : versionService.interceptors())) + .addService(interceptForward(walletsService, config.disableRateLimits ? interceptors() : walletsService.interceptors())) + .addService(interceptForward(notificationsService, config.disableRateLimits ? interceptors() : notificationsService.interceptors())) + .addService(interceptForward(moneroConnectionsService, config.disableRateLimits ? interceptors() : moneroConnectionsService.interceptors())) + .addService(interceptForward(moneroNodeService, config.disableRateLimits ? interceptors() : moneroNodeService.interceptors())) .build(); + coreContext.setApiUser(true); } + private ServerInterceptor[] interceptors() { + return new ServerInterceptor[]{callLoggingInterceptor()}; + } + + private ServerInterceptor callLoggingInterceptor() { + return new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + log.debug("GRPC endpoint called: " + call.getMethodDescriptor().getFullMethodName()); + return next.startCall(call, headers); + } + }; + } + public void start() { try { server.start(); diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java index 7c3ca22e3b..9fbfa02089 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcWalletsService.java @@ -43,6 +43,8 @@ import static haveno.core.api.model.XmrTx.toXmrTx; import haveno.daemon.grpc.interceptor.CallRateMeteringInterceptor; import haveno.daemon.grpc.interceptor.GrpcCallRateMeter; import static haveno.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import haveno.proto.grpc.CreateXmrSweepTxsReply; +import haveno.proto.grpc.CreateXmrSweepTxsRequest; import haveno.proto.grpc.CreateXmrTxReply; import haveno.proto.grpc.CreateXmrTxRequest; import haveno.proto.grpc.GetAddressBalanceReply; @@ -61,8 +63,8 @@ import haveno.proto.grpc.GetXmrTxsReply; import haveno.proto.grpc.GetXmrTxsRequest; import haveno.proto.grpc.LockWalletReply; import haveno.proto.grpc.LockWalletRequest; -import haveno.proto.grpc.RelayXmrTxReply; -import haveno.proto.grpc.RelayXmrTxRequest; +import haveno.proto.grpc.RelayXmrTxsReply; +import haveno.proto.grpc.RelayXmrTxsRequest; import haveno.proto.grpc.RemoveWalletPasswordReply; import haveno.proto.grpc.RemoveWalletPasswordRequest; import haveno.proto.grpc.SetWalletPasswordReply; @@ -185,7 +187,7 @@ class GrpcWalletsService extends WalletsImplBase { .stream() .map(s -> new MoneroDestination(s.getAddress(), new BigInteger(s.getAmount()))) .collect(Collectors.toList())); - log.info("Successfully created XMR tx: hash {}", tx.getHash()); + log.info("Successfully created XMR tx, hash: {}", tx.getHash()); var reply = CreateXmrTxReply.newBuilder() .setTx(toXmrTx(tx).toProtoMessage()) .build(); @@ -197,12 +199,30 @@ class GrpcWalletsService extends WalletsImplBase { } @Override - public void relayXmrTx(RelayXmrTxRequest req, - StreamObserver responseObserver) { + public void createXmrSweepTxs(CreateXmrSweepTxsRequest req, + StreamObserver responseObserver) { try { - String txHash = coreApi.relayXmrTx(req.getMetadata()); - var reply = RelayXmrTxReply.newBuilder() - .setHash(txHash) + List xmrTxs = coreApi.createXmrSweepTxs(req.getAddress()); + log.info("Successfully created XMR sweep txs, hashes: {}", xmrTxs.stream().map(MoneroTxWallet::getHash).collect(Collectors.toList())); + var reply = CreateXmrSweepTxsReply.newBuilder() + .addAllTxs(xmrTxs.stream() + .map(s -> toXmrTx(s).toProtoMessage()) + .collect(Collectors.toList())) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void relayXmrTxs(RelayXmrTxsRequest req, + StreamObserver responseObserver) { + try { + List txHashes = coreApi.relayXmrTxs(req.getMetadatasList()); + var reply = RelayXmrTxsReply.newBuilder() + .addAllHashes(txHashes) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index fc5f50c2b5..e4c2f20c8f 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/linux/jpackage.deb/Haveno.desktop b/desktop/package/linux/jpackage.deb/Haveno.desktop new file mode 100644 index 0000000000..a12d149029 --- /dev/null +++ b/desktop/package/linux/jpackage.deb/Haveno.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Haveno +GenericName=Monero Exchange +Comment=A decentralized monero exchange network. +Exec=/opt/haveno/bin/Haveno +Icon=/opt/haveno/lib/Haveno.png +Terminal=false +Type=Application +Categories=Network +MimeType= +StartupWMClass=haveno.desktop.app.HavenoApp diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index a24f430c12..a20a90ba31 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.19 + 1.2.1 CFBundleShortVersionString - 1.0.19 + 1.2.1 CFBundleExecutable Haveno diff --git a/desktop/package/package.gradle b/desktop/package/package.gradle index 65e09d554f..38927a1373 100644 --- a/desktop/package/package.gradle +++ b/desktop/package/package.gradle @@ -252,7 +252,7 @@ task packageInstallers { } //String appDescription = 'A decentralized monero exchange network.' - String appCopyright = '© 2024 Haveno' + String appCopyright = '© 2025 Haveno' String appNameAndVendor = 'Haveno' String commonOpts = new String( @@ -321,6 +321,7 @@ task packageInstallers { // Package deb executeCmd(jPackageFilePath + commonOpts + linuxOpts + + " --resource-dir \"${project(':desktop').projectDir}/package/linux/jpackage.deb\"" + " --linux-deb-maintainer noreply@haveno.exchange" + " --type deb") @@ -338,13 +339,10 @@ task packageInstallers { "\"${binariesFolderPath}/${appNameAndVendor}\"" ) - // Which version of AppImageTool to use - String AppImageToolVersion = "13"; - // Download AppImageTool Map AppImageToolBinaries = [ - 'linux' : "https://github.com/AppImage/AppImageKit/releases/download/${AppImageToolVersion}/appimagetool-x86_64.AppImage", - 'linux-aarch64' : "https://github.com/AppImage/AppImageKit/releases/download/${AppImageToolVersion}/appimagetool-aarch64.AppImage", + 'linux' : "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage", + 'linux-aarch64' : "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-aarch64.AppImage", ] String osKey = getJavaBinariesDownloadURLs.property('osKey') diff --git a/desktop/src/main/java/haveno/desktop/CandleStickChart.css b/desktop/src/main/java/haveno/desktop/CandleStickChart.css index 376e0db666..4e5a05d7ea 100644 --- a/desktop/src/main/java/haveno/desktop/CandleStickChart.css +++ b/desktop/src/main/java/haveno/desktop/CandleStickChart.css @@ -62,6 +62,8 @@ -demo-bar-fill: -bs-sell; -fx-background-color: -demo-bar-fill; -fx-background-insets: 0; + -fx-background-radius: 2px; + -fx-border-radius: 2px; } .candlestick-bar.close-above-open { @@ -80,6 +82,8 @@ -fx-padding: 5; -fx-background-color: -bs-volume-transparent; -fx-background-insets: 0; + -fx-background-radius: 2px; + -fx-border-radius: 2px; } .chart-alternative-row-fill { diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index 16370c2cdb..41b5ad09bc 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -74,6 +74,7 @@ import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import javafx.stage.Modality; import javafx.stage.Screen; import javafx.stage.Stage; @@ -223,6 +224,9 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); }); CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader); + + // set initial background color + scene.setFill(CssTheme.isDarkTheme() ? Color.BLACK : Color.WHITE); return scene; } diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index ed7e956eba..c6217cfe09 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -216,7 +216,10 @@ public class HavenoAppMain extends HavenoExecutable { // Set the dialog content VBox vbox = new VBox(10); - vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField, versionField); + ImageView logoImageView = new ImageView(ImageUtil.getImageByPath("logo_splash_light_mode.png")); + logoImageView.setFitWidth(342); + logoImageView.setPreserveRatio(true); + vbox.getChildren().addAll(logoImageView, passwordField, errorMessageField, versionField); vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); diff --git a/desktop/src/main/java/haveno/desktop/components/AddressTextField.java b/desktop/src/main/java/haveno/desktop/components/AddressTextField.java index ec6f284379..8edfbd222a 100644 --- a/desktop/src/main/java/haveno/desktop/components/AddressTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/AddressTextField.java @@ -20,14 +20,17 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; @@ -55,6 +58,7 @@ public class AddressTextField extends AnchorPane { textField.setId("address-text-field"); textField.setEditable(false); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.textProperty().bind(address); @@ -70,28 +74,32 @@ public class AddressTextField extends AnchorPane { textField.focusTraversableProperty().set(focusTraversableProperty().get()); Label extWalletIcon = new Label(); - extWalletIcon.setLayoutY(3); + extWalletIcon.setLayoutY(Layout.FLOATING_ICON_Y); extWalletIcon.getStyleClass().addAll("icon", "highlight"); extWalletIcon.setTooltip(new Tooltip(tooltipText)); AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN); extWalletIcon.setOnMouseClicked(e -> openWallet()); - Label copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyLabel, new Tooltip(Res.get("addressTextField.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { if (address.get() != null && address.get().length() > 0) Utilities.copyToClipboard(address.get()); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); }); - AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(extWalletIcon, 5.0); AnchorPane.setRightAnchor(textField, 55.0); AnchorPane.setLeftAnchor(textField, 0.0); - getChildren().addAll(textField, copyIcon, extWalletIcon); + getChildren().addAll(textField, copyLabel, extWalletIcon); } private void openWallet() { diff --git a/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java b/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java index 83261ba212..11521e93a1 100644 --- a/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java +++ b/desktop/src/main/java/haveno/desktop/components/AutoTooltipButton.java @@ -31,15 +31,15 @@ public class AutoTooltipButton extends JFXButton { } public AutoTooltipButton(String text) { - super(text.toUpperCase()); + super(text); } public AutoTooltipButton(String text, Node graphic) { - super(text.toUpperCase(), graphic); + super(text, graphic); } public void updateText(String text) { - setText(text.toUpperCase()); + setText(text); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java index 2b1adc5769..e6ef5dca7c 100644 --- a/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java +++ b/desktop/src/main/java/haveno/desktop/components/AutocompleteComboBox.java @@ -24,6 +24,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventHandler; +import javafx.scene.control.ListView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import org.apache.commons.lang3.StringUtils; @@ -44,6 +45,8 @@ public class AutocompleteComboBox extends JFXComboBox { private List extendedList; private List matchingList; private JFXComboBoxListViewSkin comboBoxListViewSkin; + private boolean selectAllShortcut = false; + private T lastCommittedValue; public AutocompleteComboBox() { this(FXCollections.observableArrayList()); @@ -57,6 +60,30 @@ public class AutocompleteComboBox extends JFXComboBox { fixSpaceKey(); setAutocompleteItems(items); reactToQueryChanges(); + + // Store last committed value so we can restore it if nothing selected + valueProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null) + lastCommittedValue = newVal; + }); + + // Restore last committed value when editor loses focus if no matches + getEditor().focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { + if (!isNowFocused) { + String input = getEditor().getText(); + T matched = getConverter().fromString(input); + + boolean matchFound = getItems().stream() + .anyMatch(item -> item.equals(matched)); + + if (!matchFound) { + UserThread.execute(() -> { + getSelectionModel().select(lastCommittedValue); + getEditor().setText(asString(lastCommittedValue)); + }); + } + } + }); } /** @@ -98,10 +125,16 @@ public class AutocompleteComboBox extends JFXComboBox { return; } - // Case 2: fire if the text is empty to support special "show all" case + // Case 2: fire if the text is empty if (inputText.isEmpty()) { eh.handle(e); getParent().requestFocus(); + + // Restore the last committed value + UserThread.execute(() -> { + getSelectionModel().select(lastCommittedValue); + getEditor().setText(asString(lastCommittedValue)); + }); } }); } @@ -153,6 +186,27 @@ public class AutocompleteComboBox extends JFXComboBox { private void reactToQueryChanges() { getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { + + // ignore ctrl and command keys + if (event.getCode() == KeyCode.CONTROL || event.getCode() == KeyCode.COMMAND || event.getCode() == KeyCode.META) { + event.consume(); + return; + } + + // handle select all + boolean isSelectAll = event.getCode() == KeyCode.A && (event.isControlDown() || event.isMetaDown()); + if (isSelectAll) { + getEditor().selectAll(); + selectAllShortcut = true; + event.consume(); + return; + } + if (event.getCode() == KeyCode.A && selectAllShortcut) { // 'A' can be received after ctrl/cmd + selectAllShortcut = false; + event.consume(); + return; + } + UserThread.execute(() -> { String query = getEditor().getText(); var exactMatch = list.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query)); @@ -180,6 +234,10 @@ public class AutocompleteComboBox extends JFXComboBox { if (matchingListSize() > 0) { comboBoxListViewSkin.getPopupContent().autosize(); show(); + if (comboBoxListViewSkin.getPopupContent() instanceof ListView listView) { + listView.applyCss(); + listView.layout(); + } } else { hide(); } diff --git a/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java b/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java index 4e00789dc8..7775b0b812 100644 --- a/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/BalanceTextField.java @@ -47,6 +47,7 @@ public class BalanceTextField extends AnchorPane { public BalanceTextField(String label) { textField = new HavenoTextField(); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setPromptText(label); textField.setFocusTraversable(false); textField.setEditable(false); diff --git a/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java b/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java index 7dff009b9e..7e31272ca8 100644 --- a/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/ExplorerAddressTextField.java @@ -23,6 +23,7 @@ import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.user.Preferences; +import haveno.desktop.util.GUIUtil; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -38,19 +39,19 @@ public class ExplorerAddressTextField extends AnchorPane { @Getter private final TextField textField; - private final Label copyIcon, missingAddressWarningIcon; + private final Label copyLabel, missingAddressWarningIcon; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ExplorerAddressTextField() { - copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - AnchorPane.setRightAnchor(copyIcon, 30.0); + copyLabel = new Label(); + copyLabel.setLayoutY(3); + copyLabel.getStyleClass().addAll("icon", "highlight"); + copyLabel.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("explorerAddressTextField.blockExplorerIcon.tooltip")); @@ -71,27 +72,27 @@ public class ExplorerAddressTextField extends AnchorPane { AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, missingAddressWarningIcon, copyIcon); + getChildren().addAll(textField, missingAddressWarningIcon, copyLabel); } public void setup(@Nullable String address) { if (address == null) { textField.setText(Res.get("shared.na")); textField.setId("address-text-field-error"); - copyIcon.setVisible(false); - copyIcon.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setManaged(false); missingAddressWarningIcon.setVisible(true); missingAddressWarningIcon.setManaged(true); return; } textField.setText(address); - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); + copyLabel.setOnMouseClicked(e -> Utilities.copyToClipboard(address)); } public void cleanup() { textField.setOnMouseClicked(null); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); textField.setText(""); } } diff --git a/desktop/src/main/java/haveno/desktop/components/FundsTextField.java b/desktop/src/main/java/haveno/desktop/components/FundsTextField.java index 0804750972..aa01093469 100644 --- a/desktop/src/main/java/haveno/desktop/components/FundsTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/FundsTextField.java @@ -17,9 +17,10 @@ package haveno.desktop.components; -import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; +import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -29,8 +30,6 @@ import javafx.scene.layout.AnchorPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static haveno.desktop.util.FormBuilder.getIcon; - public class FundsTextField extends InfoTextField { public static final Logger log = LoggerFactory.getLogger(FundsTextField.class); @@ -46,11 +45,12 @@ public class FundsTextField extends InfoTextField { textField.textProperty().unbind(); textField.textProperty().bind(Bindings.concat(textProperty())); // TODO: removed `, " ", fundsStructure` for haveno to fix "Funds needed: .123 XMR (null)" bug - Label copyIcon = getIcon(AwesomeIcon.COPY); - copyIcon.setLayoutY(5); - copyIcon.getStyleClass().addAll("icon", "highlight"); - Tooltip.install(copyIcon, new Tooltip(Res.get("shared.copyToClipboard"))); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(copyLabel, new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; @@ -64,11 +64,11 @@ public class FundsTextField extends InfoTextField { } }); - AnchorPane.setRightAnchor(copyIcon, 30.0); + AnchorPane.setRightAnchor(copyLabel, 30.0); AnchorPane.setRightAnchor(infoIcon, 62.0); AnchorPane.setRightAnchor(textField, 55.0); - getChildren().add(copyIcon); + getChildren().add(copyLabel); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java b/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java index 96c9e2571d..bb8c24eecf 100644 --- a/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/HavenoTextField.java @@ -1,16 +1,18 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; +import haveno.desktop.util.GUIUtil; import javafx.scene.control.Skin; public class HavenoTextField extends JFXTextField { public HavenoTextField(String value) { super(value); + GUIUtil.applyFilledStyle(this); } public HavenoTextField() { - super(); + this(null); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/InfoTextField.java b/desktop/src/main/java/haveno/desktop/components/InfoTextField.java index beafcf9494..7e47b9339f 100644 --- a/desktop/src/main/java/haveno/desktop/components/InfoTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/InfoTextField.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.desktop.components.controlsfx.control.PopOver; +import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Node; @@ -51,13 +52,14 @@ public class InfoTextField extends AnchorPane { arrowLocation = PopOver.ArrowLocation.RIGHT_TOP; textField = new HavenoTextField(); textField.setLabelFloat(true); + textField.getStyleClass().add("label-float"); textField.setEditable(false); textField.textProperty().bind(text); textField.setFocusTraversable(false); textField.setId("info-field"); infoIcon = getIcon(AwesomeIcon.INFO_SIGN); - infoIcon.setLayoutY(5); + infoIcon.setLayoutY(Layout.FLOATING_ICON_Y - 2); infoIcon.getStyleClass().addAll("icon", "info"); AnchorPane.setRightAnchor(infoIcon, 7.0); diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextField.java b/desktop/src/main/java/haveno/desktop/components/InputTextField.java index 8c4ace02b8..b46e7f84b1 100644 --- a/desktop/src/main/java/haveno/desktop/components/InputTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/InputTextField.java @@ -20,6 +20,7 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import haveno.core.util.validation.InputValidator; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.validation.JFXInputValidator; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -67,6 +68,7 @@ public class InputTextField extends JFXTextField { public InputTextField() { super(); + GUIUtil.applyFilledStyle(this); getValidators().add(jfxValidationWrapper); diff --git a/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java index da8dcb4a70..af854c437f 100644 --- a/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/PeerInfoIcon.java @@ -124,7 +124,8 @@ public class PeerInfoIcon extends Group { numTradesPane.relocate(scaleFactor * 18, scaleFactor * 14); numTradesPane.setMouseTransparent(true); ImageView numTradesCircle = new ImageView(); - numTradesCircle.setId("image-green_circle"); + numTradesCircle.setId("image-green_circle_solid"); + numTradesLabel = new AutoTooltipLabel(); numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1); numTradesLabel.setId("ident-num-label"); @@ -134,7 +135,7 @@ public class PeerInfoIcon extends Group { tagPane.relocate(Math.round(scaleFactor * 18), scaleFactor * -2); tagPane.setMouseTransparent(true); ImageView tagCircle = new ImageView(); - tagCircle.setId("image-blue_circle"); + tagCircle.setId("image-blue_circle_solid"); tagLabel = new AutoTooltipLabel(); tagLabel.relocate(Math.round(scaleFactor * 5), scaleFactor * 1); tagLabel.setId("ident-num-label"); diff --git a/desktop/src/main/java/haveno/desktop/components/PeerInfoIconTrading.java b/desktop/src/main/java/haveno/desktop/components/PeerInfoIconTrading.java index 6a9f1344d6..47fe298dd1 100644 --- a/desktop/src/main/java/haveno/desktop/components/PeerInfoIconTrading.java +++ b/desktop/src/main/java/haveno/desktop/components/PeerInfoIconTrading.java @@ -198,6 +198,6 @@ public class PeerInfoIconTrading extends PeerInfoIcon { Offer offerToCheck = Trade != null ? Trade.getOffer() : offer; return offerToCheck != null && - PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCurrencyCode()); + PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCounterCurrencyCode()); } } diff --git a/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java b/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java index d337916da3..1dbd47eaaa 100644 --- a/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/TextFieldWithCopyIcon.java @@ -18,12 +18,15 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; -import de.jensd.fx.fontawesome.AwesomeDude; -import de.jensd.fx.fontawesome.AwesomeIcon; + +import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; +import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -45,12 +48,13 @@ public class TextFieldWithCopyIcon extends AnchorPane { } public TextFieldWithCopyIcon(String customStyleClass) { - Label copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - copyIcon.setOnMouseClicked(e -> { + Label copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + if (customStyleClass != null) copyLabel.getStyleClass().add(customStyleClass + "-icon"); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setOnMouseClicked(e -> { String text = getText(); if (text != null && text.length() > 0) { String copyText; @@ -70,17 +74,25 @@ public class TextFieldWithCopyIcon extends AnchorPane { copyText = text; } Utilities.copyToClipboard(copyText); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); } }); textField = new JFXTextField(); textField.setEditable(false); if (customStyleClass != null) textField.getStyleClass().add(customStyleClass); textField.textProperty().bindBidirectional(text); - AnchorPane.setRightAnchor(copyIcon, 5.0); + AnchorPane.setRightAnchor(copyLabel, 5.0); AnchorPane.setRightAnchor(textField, 30.0); AnchorPane.setLeftAnchor(textField, 0.0); + AnchorPane.setTopAnchor(copyLabel, 0.0); + AnchorPane.setBottomAnchor(copyLabel, 0.0); + AnchorPane.setTopAnchor(textField, 0.0); + AnchorPane.setBottomAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, copyIcon); + getChildren().addAll(textField, copyLabel); } public void setPromptText(String value) { diff --git a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java index 2e66a0e026..9c7e8a823d 100644 --- a/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java +++ b/desktop/src/main/java/haveno/desktop/components/TextFieldWithIcon.java @@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.UserThread; +import haveno.desktop.util.Layout; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.TextField; @@ -53,10 +54,10 @@ public class TextFieldWithIcon extends AnchorPane { iconLabel = new Label(); iconLabel.setLayoutX(0); - iconLabel.setLayoutY(3); + iconLabel.setLayoutY(Layout.FLOATING_ICON_Y - 2); dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> { - iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20); + iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20 + Layout.FLOATING_ICON_Y); }); getChildren().addAll(textField, dummyTextField, iconLabel); diff --git a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java index e9ced56cdf..2a55dba7b0 100644 --- a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java @@ -29,7 +29,10 @@ import haveno.core.user.Preferences; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.indicator.TxConfidenceIndicator; import haveno.desktop.util.GUIUtil; +import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; +import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -51,7 +54,7 @@ public class TxIdTextField extends AnchorPane { private final TextField textField; private final Tooltip progressIndicatorTooltip; private final TxConfidenceIndicator txConfidenceIndicator; - private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon; + private final Label copyLabel, blockExplorerIcon, missingTxWarningIcon; private MoneroWalletListener walletListener; private ChangeListener tradeListener; @@ -70,16 +73,17 @@ public class TxIdTextField extends AnchorPane { txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setVisible(false); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); - AnchorPane.setTopAnchor(txConfidenceIndicator, 3.0); + AnchorPane.setTopAnchor(txConfidenceIndicator, Layout.FLOATING_ICON_Y); progressIndicatorTooltip = new Tooltip("-"); txConfidenceIndicator.setTooltip(progressIndicatorTooltip); - copyIcon = new Label(); - copyIcon.setLayoutY(3); - copyIcon.getStyleClass().addAll("icon", "highlight"); - copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); - AnchorPane.setRightAnchor(copyIcon, 30.0); + copyLabel = new Label(); + copyLabel.setLayoutY(Layout.FLOATING_ICON_Y); + copyLabel.getStyleClass().addAll("icon", "highlight"); + copyLabel.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); + copyLabel.setGraphic(GUIUtil.getCopyIcon()); + copyLabel.setCursor(Cursor.HAND); + AnchorPane.setRightAnchor(copyLabel, 30.0); Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip")); @@ -89,7 +93,7 @@ public class TxIdTextField extends AnchorPane { AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); blockExplorerIcon.setMinWidth(20); AnchorPane.setRightAnchor(blockExplorerIcon, 52.0); - AnchorPane.setTopAnchor(blockExplorerIcon, 4.0); + AnchorPane.setTopAnchor(blockExplorerIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon = new Label(); missingTxWarningIcon.getStyleClass().addAll("icon", "error-icon"); @@ -97,7 +101,7 @@ public class TxIdTextField extends AnchorPane { missingTxWarningIcon.setTooltip(new Tooltip(Res.get("txIdTextField.missingTx.warning.tooltip"))); missingTxWarningIcon.setMinWidth(20); AnchorPane.setRightAnchor(missingTxWarningIcon, 52.0); - AnchorPane.setTopAnchor(missingTxWarningIcon, 4.0); + AnchorPane.setTopAnchor(missingTxWarningIcon, Layout.FLOATING_ICON_Y); missingTxWarningIcon.setVisible(false); missingTxWarningIcon.setManaged(false); @@ -108,7 +112,7 @@ public class TxIdTextField extends AnchorPane { AnchorPane.setRightAnchor(textField, 80.0); AnchorPane.setLeftAnchor(textField, 0.0); textField.focusTraversableProperty().set(focusTraversableProperty().get()); - getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyIcon, txConfidenceIndicator); + getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyLabel, txConfidenceIndicator); } public void setup(@Nullable String txId) { @@ -131,8 +135,8 @@ public class TxIdTextField extends AnchorPane { textField.setId("address-text-field-error"); blockExplorerIcon.setVisible(false); blockExplorerIcon.setManaged(false); - copyIcon.setVisible(false); - copyIcon.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setManaged(false); txConfidenceIndicator.setVisible(false); missingTxWarningIcon.setVisible(true); missingTxWarningIcon.setManaged(true); @@ -158,7 +162,13 @@ public class TxIdTextField extends AnchorPane { textField.setText(txId); textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId)); + copyLabel.setOnMouseClicked(e -> { + Utilities.copyToClipboard(txId); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); txConfidenceIndicator.setVisible(true); // update off main thread @@ -177,7 +187,7 @@ public class TxIdTextField extends AnchorPane { trade = null; textField.setOnMouseClicked(null); blockExplorerIcon.setOnMouseClicked(null); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); textField.setText(""); } diff --git a/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java b/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java index f04dee5960..85b7eaceba 100644 --- a/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java +++ b/desktop/src/main/java/haveno/desktop/components/controlsfx/control/PopOver.java @@ -494,7 +494,7 @@ public class PopOver extends PopupControl { * @since 1.0 */ public final void hide(Duration fadeOutDuration) { - log.info("hide:" + fadeOutDuration.toString()); + log.debug("hide:" + fadeOutDuration.toString()); //We must remove EventFilter in order to prevent memory leak. if (ownerWindow != null) { ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, diff --git a/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java b/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java index 9bed491f49..017e22fac2 100644 --- a/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java +++ b/desktop/src/main/java/haveno/desktop/components/list/FilterBox.java @@ -17,13 +17,11 @@ package haveno.desktop.components.list; -import haveno.core.locale.Res; -import haveno.desktop.components.AutoTooltipLabel; +import haveno.common.UserThread; import haveno.desktop.components.InputTextField; import haveno.desktop.util.filtering.FilterableListItem; import javafx.beans.value.ChangeListener; import javafx.collections.transformation.FilteredList; -import javafx.geometry.Insets; import javafx.scene.control.TableView; import javafx.scene.layout.HBox; @@ -37,21 +35,20 @@ public class FilterBox extends HBox { super(); setSpacing(5.0); - AutoTooltipLabel label = new AutoTooltipLabel(Res.get("shared.filter")); - HBox.setMargin(label, new Insets(5.0, 0, 0, 10.0)); - textField = new InputTextField(); textField.setMinWidth(500); - getChildren().addAll(label, textField); + getChildren().addAll(textField); } public void initialize(FilteredList filteredList, TableView tableView) { this.filteredList = filteredList; listener = (observable, oldValue, newValue) -> { - tableView.getSelectionModel().clearSelection(); - applyFilteredListPredicate(textField.getText()); + UserThread.execute(() -> { + tableView.getSelectionModel().clearSelection(); + applyFilteredListPredicate(textField.getText()); + }); }; } @@ -67,4 +64,8 @@ public class FilterBox extends HBox { private void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(item -> item.match(filterString)); } + + public void setPromptText(String promptText) { + textField.setPromptText(promptText); + } } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java index 3bcdc44075..6bd796bf7a 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AmazonGiftCardForm.java @@ -120,7 +120,7 @@ public class AmazonGiftCardForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr()); + setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr() == null ? "" : amazonGiftCardAccount.getEmailOrMobileNr()); } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java index f57c3f154f..0548c5d4b6 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AssetsForm.java @@ -36,6 +36,7 @@ import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.InputTextField; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.FormBuilder; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.geometry.Insets; import javafx.scene.control.CheckBox; @@ -191,7 +192,7 @@ public class AssetsForm extends PaymentMethodForm { @Override protected void addTradeCurrencyComboBox() { currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.crypto"), - Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + Layout.GROUP_DISTANCE).second; currencyComboBox.setPromptText(Res.get("payment.select.crypto")); currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.crypto"), currencyComboBox)); @@ -202,6 +203,8 @@ public class AssetsForm extends PaymentMethodForm { CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); + currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactoryNameAndCode()); + currencyComboBox.setConverter(new StringConverter<>() { @Override public String toString(TradeCurrency tradeCurrency) { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java index 8447a1a01a..f57ac5cc09 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/InteracETransferForm.java @@ -44,7 +44,7 @@ public class InteracETransferForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), ((InteracETransferAccountPayload) paymentAccountPayload).getHolderName()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.emailOrMobile"), - ((InteracETransferAccountPayload) paymentAccountPayload).getEmail()); + ((InteracETransferAccountPayload) paymentAccountPayload).getEmailOrMobileNr()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.secret"), ((InteracETransferAccountPayload) paymentAccountPayload).getQuestion()); addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.answer"), diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java index 922b5aec75..6ed90bfebf 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/TransferwiseUsdForm.java @@ -57,7 +57,7 @@ public class TransferwiseUsdForm extends PaymentMethodForm { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 1, Res.get("payment.email"), ((TransferwiseUsdAccountPayload) paymentAccountPayload).getEmail()); - String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getBeneficiaryAddress(); + String address = ((TransferwiseUsdAccountPayload) paymentAccountPayload).getHolderAddress(); if (address.length() > 0) { TextArea textAddress = addCompactTopLabelTextArea(gridPane, gridRow, 0, Res.get("payment.account.address"), "").second; textAddress.setMinHeight(70); @@ -96,7 +96,7 @@ public class TransferwiseUsdForm extends PaymentMethodForm { updateFromInputs(); }); - String addressLabel = Res.get("payment.account.owner.address") + Res.get("payment.transferwiseUsd.address"); + String addressLabel = Res.get("payment.account.owner.address") + " " + Res.get("payment.transferwiseUsd.address"); TextArea addressTextArea = addTopLabelTextArea(gridPane, ++gridRow, addressLabel, addressLabel).second; addressTextArea.setMinHeight(70); addressTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index e3cfac8c0e..3eae67086b 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -39,7 +39,7 @@ -fx-text-fill: -bs-color-primary; } -.highlight, .highlight-static { +.highlight, .highlight-static, .highlight.label .glyph-icon { -fx-text-fill: -fx-accent; -fx-fill: -fx-accent; } @@ -105,6 +105,11 @@ -fx-font-size: 1.077em; -fx-font-family: "IBM Plex Mono"; -fx-padding: 0 !important; + -fx-border-width: 0; + -fx-text-fill: -bs-rd-font-dark-gray !important; +} + +.confirmation-text-field-as-label-icon { } /* Other UI Elements */ @@ -150,9 +155,9 @@ -fx-text-fill: -bs-rd-font-dark-gray; -fx-font-size: 0.923em; -fx-font-weight: normal; - -fx-background-radius: 2px; - -fx-pref-height: 32; - -fx-min-height: -fx-pref-height; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-min-height: 32; -fx-padding: 0 40 0 40; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 2, 0, 0, 0, 1); -fx-cursor: hand; @@ -171,8 +176,12 @@ -fx-text-fill: -bs-background-color; } -.compact-button, .table-cell .jfx-button, .action-button.compact-button { - -fx-padding: 0 10 0 10; +.action-button.compact-button, .compact-button { + -fx-padding: 0 15 0 15; +} + +.table-cell .jfx-button { + -fx-padding: 0 7 0 7; } .tiny-button, @@ -217,10 +226,59 @@ -fx-border-width: 1; } +.jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field, .toggle-button-no-slider { + -fx-padding: 7 14 7 14; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-border-color: transparent; +} + .jfx-combo-box { - -jfx-focus-color: -bs-color-primary; - -jfx-unfocus-color: -bs-color-gray-line; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-form-field; +} + +.input-line, .input-focused-line { + -fx-background-color: transparent; + visibility: hidden; + -fx-max-height: 0; +} + +.jfx-text-field { + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-background-color: -bs-color-background-form-field; +} + +.jfx-text-field.label-float .prompt-container { + -fx-translate-y: 0px; +} + +.jfx-text-field.filled.label-float .prompt-container, +.jfx-text-field.label-float:focused .prompt-container, +.jfx-combo-box.filled.label-float .prompt-container, +.jfx-combo-box.label-float:focused .prompt-container, +.jfx-password-field.filled.label-float .prompt-container, +.jfx-password-field.label-float:focused .prompt-container { + -fx-translate-x: -14px; + -fx-translate-y: -5.5px; +} + +.jfx-combo-box .arrow-button { + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 0 0 10; +} + +.jfx-combo-box:hover { + -fx-cursor: hand; +} + +.jfx-combo-box:editable:hover { + -fx-cursor: null; +} + +.jfx-combo-box .arrow-button:hover { + -fx-cursor: hand; } .jfx-combo-box > .list-cell { @@ -228,15 +286,66 @@ -fx-font-family: "IBM Plex Sans Medium"; } +/* TODO: otherwise combo box with "odd" class is opacity 0.4? */ +.jfx-combo-box > .list-cell:odd, .jfx-combo-box > .list-cell:even { + -fx-opacity: 1.0; +} + +.jfx-combo-box > .list-cell, +.jfx-combo-box > .text-field { + -fx-padding: 0 !important; +} + .jfx-combo-box > .arrow-button > .arrow { -fx-background-color: null; - -fx-border-color: -jfx-unfocus-color; + -fx-border-color: -bs-color-gray-line; -fx-shape: "M 0 0 l 3.5 4 l 3.5 -4"; } +.combo-box-popup { + -fx-background-color: -bs-color-background-pane; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +.combo-box-popup .scroll-pane { + -fx-background-color: -bs-color-background-pane; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +.combo-box-popup > .list-view { + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-border-form-field; + -fx-translate-y: 4; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 5; +} + +/* Rounds the first and last list cells to create full round illusion */ +.combo-box-popup .list-cell:first-child { + -fx-background-radius: 10 10 0 0; +} +.combo-box-popup .list-cell:last-child { + -fx-background-radius: 0 0 10 10; +} + +.combo-box-popup .list-cell:hover { + -fx-background-radius: 8; +} + +.combo-box-popup > .list-view:hover { + -fx-cursor: hand; +} + .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background: -fx-selection-bar; -fx-background-color: -fx-selection-bar; + -fx-background-radius: 15; + -fx-border-radius: 15; } .combo-box-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover, @@ -301,19 +410,12 @@ tree-table-view:focused { -fx-background-insets: 0; } -.jfx-text-field { - -jfx-focus-color: -bs-color-primary; - -fx-background-color: -bs-background-color; - -fx-background-radius: 3 3 0 0; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; +/* combo box list view */ +.combo-box .list-view .list-cell:odd { + -fx-background-color: -bs-color-background-pane; } - -.jfx-text-field > .input-line { - -fx-translate-x: -0.333333em; -} - -.jfx-text-field > .input-focused-line { - -fx-translate-x: -0.333333em; +.combo-box .list-view .list-cell:even { + -fx-background-color: -bs-color-background-pane; } .jfx-text-field-top-label { @@ -321,8 +423,7 @@ tree-table-view:focused { } .jfx-text-field:readonly, .hyperlink-with-icon { - -fx-background-color: -bs-color-gray-1; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field-readonly; } .jfx-text-field:readonly > .input-line { @@ -348,23 +449,16 @@ tree-table-view:focused { } .jfx-password-field { - -fx-background-color: -bs-background-color; - -fx-background-radius: 3 3 0 0; - -jfx-focus-color: -bs-color-primary; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field; } .jfx-password-field > .input-line { -fx-translate-x: -0.333333em; } -.jfx-password-field > .input-focused-line { - -fx-translate-x: -0.333333em; -} - -.jfx-text-field:error, .jfx-password-field:error, .jfx-text-area:error { - -jfx-focus-color: -bs-rd-error-red; - -jfx-unfocus-color: -bs-rd-error-red; +.jfx-combo-box:error, +.jfx-text-field:error { + -fx-text-fill: -bs-rd-error-red; } .jfx-text-field .error-label, .jfx-password-field .error-label, .jfx-text-area .error-label { @@ -378,58 +472,69 @@ tree-table-view:focused { -fx-font-size: 1em; } -.input-with-border { - -fx-background-color: -bs-background-color; - -fx-border-width: 1; +.offer-input { + -fx-background-color: -bs-color-background-form-field; -fx-border-color: -bs-background-gray; - -fx-border-radius: 3; -fx-pref-height: 43; -fx-pref-width: 310; - -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); -} - -.input-with-border .text-field { - -fx-alignment: center-right; - -fx-pref-height: 43; - -fx-font-size: 1.385em; -} - -.input-with-border > .input-label { - -fx-font-size: 0.692em; - -fx-min-width: 60; - -fx-padding: 16; + -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 0); + -fx-background-radius: 999; + -fx-border-radius: 999; -fx-alignment: center; } -.input-with-border .icon { +.offer-input .text-field { + -fx-alignment: center-right; + -fx-pref-height: 44; + -fx-font-size: 1.385em; + -fx-background-radius: 999 0 0 999; + -fx-border-radius: 999 0 0 999; + -fx-background-color: -bs-color-background-form-field; + -fx-border-color: transparent; +} + +.offer-input > .input-label { + -fx-font-size: 0.692em; + -fx-min-width: 45; + -fx-padding: 8; + -fx-alignment: center; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-background-color: derive(-bs-color-background-form-field, 15%); +} + +.offer-input .icon { -fx-padding: 10; } -.input-with-border-readonly { +.offer-input-readonly { -fx-background-color: -bs-color-gray-1; -fx-border-width: 0; -fx-pref-width: 300; + -fx-background-radius: 999; + -fx-border-radius: 999; } -.input-with-border-readonly .text-field { +.offer-input-readonly .text-field { -fx-alignment: center-right; -fx-font-size: 1em; -fx-background-color: -bs-color-gray-1; + -fx-border-width: 0; } -.input-with-border-readonly .text-field > .input-line { +.offer-input-readonly .text-field > .input-line { -fx-background-color: transparent; } -.input-with-border-readonly > .input-label { +.offer-input-readonly > .input-label { -fx-font-size: 0.692em; -fx-min-width: 30; -fx-padding: 8; -fx-alignment: center; } -.input-with-border-readonly .icon { - -fx-padding: 2; +.offer-input-readonly .icon { + -fx-padding: 3; } .jfx-badge .badge-pane { @@ -456,7 +561,8 @@ tree-table-view:focused { } .jfx-badge { - -fx-padding: -3 0 0 0; + -fx-padding: -2 0 0 0; + -fx-border-insets: 0 0 0 0; } .jfx-toggle-button, @@ -469,11 +575,15 @@ tree-table-view:focused { -jfx-size: 8; } +.jfx-toggle-button:hover { + -fx-cursor: hand; +} + .jfx-text-area { - -jfx-focus-color: -bs-color-primary; - -jfx-unfocus-color: -bs-color-gray-line; - -fx-background-color: -bs-background-color; - -fx-padding: 0.333333em 0.333333em 0.333333em 0.333333em; + -fx-background-color: -bs-color-background-form-field; + -fx-padding: 9 9 9 9; + -fx-background-radius: 15; + -fx-border-radius: 15; } .jfx-text-area:readonly { @@ -484,8 +594,10 @@ tree-table-view:focused { -fx-translate-x: -0.333333em; } -.jfx-text-area > .input-focused-line { - -fx-translate-x: -0.333333em; +.text-area .viewport { + -fx-background-color: transparent; + -fx-background-radius: 15; + -fx-border-radius: 15; } .wallet-seed-words { @@ -501,16 +613,18 @@ tree-table-view:focused { -jfx-default-color: -bs-color-primary; } -.jfx-date-picker .jfx-text-field .jfx-text-area { +.jfx-date-picker { -fx-padding: 0.333333em 0em 0.333333em 0em; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; + -fx-background-color: transparent; } .jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; + -fx-background-color: transparent; } .jfx-date-picker > .arrow-button > .arrow { @@ -535,14 +649,14 @@ tree-table-view:focused { .scroll-bar:horizontal .track, .scroll-bar:vertical .track { - -fx-background-color: -bs-background-color; - -fx-border-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-background-pane; -fx-background-radius: 0; } .scroll-bar:vertical .track-background, .scroll-bar:horizontal .track-background { - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; -fx-background-insets: 0; -fx-background-radius: 0; } @@ -573,7 +687,7 @@ tree-table-view:focused { .scroll-bar:vertical .decrement-button, .scroll-bar:horizontal .increment-button, .scroll-bar:horizontal .decrement-button { - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; -fx-padding: 1; } @@ -582,12 +696,12 @@ tree-table-view:focused { .scroll-bar:horizontal .decrement-arrow, .scroll-bar:vertical .decrement-arrow { -fx-shape: null; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; } .scroll-bar:vertical:focused, .scroll-bar:horizontal:focused { - -fx-background-color: -bs-background-color, -bs-color-gray-ccc, -bs-color-gray-ddd; + -fx-background-color: -bs-color-background-pane; } /* Behavior */ @@ -632,7 +746,7 @@ tree-table-view:focused { /* Main UI */ #base-content-container { - -fx-background-color: -bs-background-gray; + -fx-background-color: -bs-color-gray-background; } .content-pane { @@ -657,10 +771,10 @@ tree-table-view:focused { /* Main navigation */ .top-navigation { -fx-background-color: -bs-rd-nav-background; - -fx-border-width: 0 0 1 0; + -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; - -fx-pref-height: 57; - -fx-padding: 0 11 0 0; + -fx-background-radius: 999; + -fx-border-radius: 999; } .top-navigation .separator:vertical .line { @@ -669,50 +783,54 @@ tree-table-view:focused { -fx-border-insets: 0 0 0 1; } +.nav-logo { + -fx-max-width: 190; + -fx-min-width: 155; +} + .nav-primary { -fx-background-color: -bs-rd-nav-primary-background; - -fx-padding: 0 11 0 11; - -fx-border-width: 0 1 0 0; + -fx-border-width: 0 0 0 0; -fx-border-color: -bs-rd-nav-primary-border; - -fx-min-width: 410; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 9 0 9 20; } .nav-secondary { - -fx-padding: 0 11 0 11; - -fx-min-width: 296; + -fx-padding: 0 14 0 0; } -.nav-price-balance { - -fx-background-color: -bs-color-gray-background; - -fx-background-radius: 3; - -fx-effect: innershadow(gaussian, -bs-text-color-transparent, 3, 0, 0, 1); - -fx-pref-height: 41; - -fx-padding: 0 10 0 0; -} - -.nav-price-balance .separator:vertical .line { +.nav-separator { + -fx-max-width: 1; + -fx-min-width: 1; -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; -fx-border-width: 1; -fx-border-insets: 0 0 0 1; } -.nav-price-balance .jfx-combo-box > .input-line { - -fx-pref-height: 0px; +.nav-spacer { + -fx-max-width: 10; + -fx-min-width: 10; } -.jfx-badge > .nav-button { + +.jfx-badge > .nav-button, +.jfx-badge > .nav-secondary-button { -fx-translate-y: 1; } .nav-button { -fx-cursor: hand; -fx-background-color: transparent; - -fx-padding: 11; + -fx-padding: 9 15; + -fx-background-radius: 999; + -fx-border-radius: 999; } .nav-button .text { - -fx-font-size: 0.769em; - -fx-font-weight: bold; + -fx-font-size: 0.95em; + -fx-font-weight: 500; -fx-fill: -bs-rd-nav-deselected; } @@ -722,23 +840,84 @@ tree-table-view:focused { .nav-button:selected { -fx-background-color: -bs-background-color; - -fx-border-radius: 4; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } +.top-navigation .nav-button:hover { + -fx-background-color: -bs-rd-nav-button-hover; +} + +.nav-primary .nav-button:hover { + -fx-background-color: -bs-rd-nav-primary-button-hover; +} + .nav-button:selected .text { -fx-fill: -bs-rd-nav-selected; } +.nav-secondary-button { + -fx-cursor: hand; + -fx-padding: 9 2 9 2; + -fx-border-insets: 0 12 1 12; + -fx-border-color: transparent; + -fx-border-width: 0 0 1px 0; +} + +.nav-secondary-button .text { + -fx-font-size: 0.95em; + -fx-font-weight: 500; + -fx-fill: -bs-rd-nav-secondary-deselected; +} + +.nav-secondary-button-japanese .text { + -fx-font-size: 1em; +} + +.nav-secondary-button:selected { + -fx-border-color: transparent transparent -bs-rd-nav-secondary-selected transparent; + -fx-border-width: 0 0 1px 0; +} + +.nav-secondary-button:hover { +} + +.nav-secondary-button:selected .text { + -fx-fill: -bs-rd-nav-secondary-selected; +} + .nav-balance-display { -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance; } +.nav-price-balance { + -fx-background-color: -bs-rd-nav-background; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 20 0 20; +} + +.nav-price-balance .separator:vertical .line { + -fx-border-color: transparent transparent transparent -bs-rd-separator-dark; + -fx-border-width: 1; + -fx-border-insets: 0 0 0 1; +} + +.nav-price-balance .jfx-combo-box { + -fx-border-color: transparent; + -fx-padding: 0; + -fx-pref-width: 180; +} + +.nav-price-balance .jfx-combo-box > .input-line { + -fx-pref-height: 0px; +} + .nav-balance-label { -fx-font-size: 0.769em; -fx-alignment: center-left; -fx-text-fill: -bs-rd-font-balance-label; + -fx-padding: 0; } #nav-alert-label { @@ -794,6 +973,10 @@ tree-table-view:focused { -fx-text-fill: -bs-background-color; } +.copy-icon-disputes.label .glyph-icon { + -fx-fill: -bs-background-color; +} + .copy-icon:hover { -fx-text-fill: -bs-text-color; } @@ -948,12 +1131,18 @@ textfield */ * * ******************************************************************************/ .table-view .table-row-cell:even .table-cell { - -fx-background-color: derive(-bs-background-color, 5%); - -fx-border-color: derive(-bs-background-color,5%); + -fx-background-color: -bs-color-background-row-even; + -fx-border-color: -bs-color-background-row-even; } .table-view .table-row-cell:odd .table-cell { - -fx-background-color: derive(-bs-background-color,-5%); - -fx-border-color: derive(-bs-background-color,-5%); + -fx-background-color: -bs-color-background-row-odd; + -fx-border-color: -bs-color-background-row-odd; +} +.table-view .table-row-cell.row-faded .table-cell .text { + -fx-fill: -bs-color-table-cell-dim; +} +.cell-faded { + -fx-opacity: 0.4; } .table-view .table-row-cell:hover .table-cell, .table-view .table-row-cell:selected .table-cell { @@ -975,42 +1164,41 @@ textfield */ .table-view .table-cell { -fx-alignment: center-left; - -fx-padding: 2 0 2 0; + -fx-padding: 6 0 4 0; + -fx-text-fill: -bs-text-color; /*-fx-padding: 3 0 2 0;*/ } .table-view .table-cell.last-column { - -fx-alignment: center-right; - -fx-padding: 2 10 2 0; + -fx-padding: 6 0 4 0; } -.table-view .table-cell.last-column.avatar-column { - -fx-alignment: center; - -fx-padding: 2 0 2 0; -} - -.table-view .column-header.last-column { - -fx-padding: 0 10 0 0; -} - -.table-view .column-header.last-column .label { - -fx-alignment: center-right; -} - -.table-view .column-header.last-column.avatar-column { - -fx-padding: 0; -} - -.table-view .column-header.last-column.avatar-column .label { +.table-view .table-cell.avatar-column { -fx-alignment: center; + -fx-padding: 6 0 4 0; } .table-view .table-cell.first-column { - -fx-padding: 2 0 2 10; + -fx-padding: 6 0 4 0; +} + +.table-view .column-header.last-column .label { } .table-view .column-header.first-column { - -fx-padding: 0 0 0 10; + -fx-padding: 0 0 0 0; +} + +.table-view .column-header.last-column { + -fx-padding: 0 0 0 0; +} + +.table-view .column-header.avatar-column { + -fx-padding: 0; +} + +.table-view .column-header.avatar-column .label { + -fx-alignment: center; } .number-column.table-cell { @@ -1019,31 +1207,31 @@ textfield */ } .table-view .filler { - -fx-background-color: -bs-color-gray-0; + -fx-background-color: transparent; } .table-view { -fx-control-inner-background-alt: -fx-control-inner-background; + -fx-padding: 0; +} + +.table-view .column-header-background { + -fx-background-color: -bs-color-background-pane; + -fx-border-color: -bs-color-border-form-field; + -fx-border-width: 0 0 1 0; } .table-view .column-header .label { -fx-alignment: center-left; -fx-font-weight: normal; -fx-font-size: 0.923em; - -fx-padding: 0; + -fx-padding: 6 0 6 0; + -fx-text-fill: -bs-text-color; } .table-view .column-header { - -fx-background-color: -bs-color-gray-0; - -fx-padding: 0; -} - -.table-view .focus { - -fx-alignment: center-left; -} - -.table-view .text { - -fx-fill: -bs-text-color; + -fx-border-color: transparent; + -fx-background-color: -bs-color-background-pane; } /* horizontal scrollbars are never needed and are flickering at scaling so lets turn them off */ @@ -1051,17 +1239,6 @@ textfield */ -fx-opacity: 0; } -.table-view:focused { - -fx-background-color: -fx-box-border, -fx-control-inner-background; - -fx-background-insets: 0, 1; - -fx-padding: 1; -} - -.table-view:focused .table-row-cell:focused { - -fx-background-color: -fx-table-cell-border-color, -fx-background; - -fx-background-insets: 0, 0 0 1 0; -} - .offer-table .table-row-cell { -fx-border-color: -bs-background-color; -fx-table-cell-border-color: -bs-background-color; @@ -1141,6 +1318,46 @@ textfield */ -fx-cell-size: 47px; } +.table-view.offer-table { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .column-header.first-column { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .column-header.last-column { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.table-view.offer-table .table-row-cell { + -fx-background: -fx-accent; + -fx-background-color: -bs-color-gray-6; +} + +.offer-table-top { + -fx-background-color: -bs-color-background-pane; + -fx-padding: 15 15 5 15; + -fx-background-radius: 15 15 0 0; + -fx-border-radius: 15 15 0 0; + -fx-border-width: 0 0 0 0; +} + +.offer-table-top .label { + -fx-text-fill: -bs-text-color; + -fx-font-size: 1.1em; + -fx-font-weight: bold; +} + +.offer-table-top .jfx-button { + -fx-pref-width: 300px; + -fx-min-height: 35px; + -fx-padding: 5 25 5 25; +} + /******************************************************************************* * * * Icons * @@ -1204,17 +1421,28 @@ textfield */ -fx-border-color: -bs-background-gray; } -.text-area-no-border { - -fx-border-color: -bs-background-color; +.text-area-popup { + -fx-border-color: -bs-color-background-popup-blur; } -.text-area-no-border .content { - -fx-background-color: -bs-background-color; +.text-area-popup .content { + -fx-background-color: -bs-color-background-popup-blur; } -.text-area-no-border:focused { - -fx-focus-color: -bs-background-color; - -fx-faint-focus-color: -bs-background-color; +.text-area-popup:focused { + -fx-faint-focus-color: -bs-color-background-popup-blur; +} + +.notification-popup-bg .text-area-popup, .peer-info-popup-bg .text-area-popup { + -fx-border-color: -bs-color-background-popup; +} + +.notification-popup-bg .text-area-popup .content, .peer-info-popup-bg .text-area-popup .content{ + -fx-background-color: -bs-color-background-popup; +} + +.notification-popup-bg .text-area-popup:focused, .peer-info-popup-bg .text-area-popup:focused { + -fx-faint-focus-color: -bs-color-background-popup; } /******************************************************************************* @@ -1238,7 +1466,7 @@ textfield */ } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button .jfx-rippler { - -jfx-rippler-fill: -fx-accent; + -jfx-rippler-fill: none; } .tab:disabled .jfx-rippler { @@ -1256,7 +1484,7 @@ textfield */ } .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button { - -fx-padding: 0 0 2 0; + -fx-padding: 0 0 0 0; } .jfx-tab-pane .headers-region .tab:selected .tab-container .tab-close-button > .jfx-svg-glyph { @@ -1276,8 +1504,8 @@ textfield */ .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-text-fill: -bs-rd-font-light; - -fx-padding: 14; - -fx-font-size: 0.769em; + -fx-padding: 9 14; + -fx-font-size: .95em; -fx-font-weight: normal; -fx-cursor: hand; } @@ -1291,7 +1519,7 @@ textfield */ } .jfx-tab-pane .headers-region > .tab > .jfx-rippler { - -jfx-rippler-fill: -fx-accent; + -jfx-rippler-fill: none; } .jfx-tab-pane .headers-region .tab:closable { @@ -1395,7 +1623,7 @@ textfield */ } #payment-info { - -fx-background-color: -bs-content-background-gray; + -fx-background-color: -bs-color-gray-fafa; } .toggle-button-active { @@ -1406,6 +1634,19 @@ textfield */ -fx-background-color: -bs-color-gray-1; } +.toggle-button-no-slider { + -fx-border-width: 1px; + -fx-border-color: -bs-color-border-form-field; + -fx-background-insets: 0; + -fx-pref-height: 36px; + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +.toggle-button-no-slider:hover { + -fx-cursor: hand; +} + #trade-fee-textfield { -fx-font-size: 0.9em; -fx-alignment: center-right; @@ -1425,7 +1666,7 @@ textfield */ .combo-box-editor-bold { -fx-font-weight: bold; - -fx-padding: 5 8 5 8 !important; + -fx-padding: 0 !important; -fx-text-fill: -bs-text-color; -fx-font-family: "IBM Plex Sans Medium"; } @@ -1456,6 +1697,15 @@ textfield */ -fx-pref-height: 35px; } +.offer-label { + -fx-background-color: rgb(50, 95, 182); + -fx-text-fill: white; + -fx-font-weight: normal; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-padding: 0 6 0 6; +} + /* Offer */ .percentage-label { -fx-alignment: center; @@ -1619,7 +1869,7 @@ textfield */ .titled-group-bg, .titled-group-bg-active { -fx-body-color: -bs-color-gray-background; -fx-border-color: -bs-rd-separator; - -fx-border-width: 0 0 1 0; + -fx-border-width: 0 0 0 0; -fx-background-color: transparent; -fx-background-insets: 0; } @@ -1729,11 +1979,23 @@ textfield */ * * ******************************************************************************/ .grid-pane { - -fx-background-color: -bs-content-background-gray; - -fx-background-radius: 5; - -fx-effect: null; - -fx-effect: dropshadow(gaussian, -bs-color-gray-10, 10, 0, 0, 0); + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 10; + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-padding: 35, 40, 30, 40; +} + +.grid-pane-separator { + -fx-border-color: -bs-rd-separator; + -fx-border-width: 0 0 1 0; + -fx-translate-y: -2; +} + +.grid-pane .text-area { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-text-fill: -bs-text-color; } /******************************************************************************************************************** @@ -1762,24 +2024,26 @@ textfield */ -fx-text-alignment: center; } -#charts .chart-plot-background, #charts-dao .chart-plot-background { - -fx-background-color: -bs-background-color; +.chart-pane, .chart-plot-background, #charts .chart-plot-background { + -fx-background-color: transparent; } #charts .default-color0.chart-area-symbol { - -fx-background-color: -bs-sell, -bs-background-color; -} - -#charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { -fx-background-color: -bs-buy, -bs-background-color; } +#charts .default-color1.chart-area-symbol, #charts-dao .default-color0.chart-area-symbol { + -fx-background-color: -bs-sell, -bs-background-color; +} + #charts .default-color0.chart-series-area-line { - -fx-stroke: -bs-sell; + -fx-stroke: -bs-buy; + -fx-stroke-width: 2px; } #charts .default-color1.chart-series-area-line, #charts-dao .default-color0.chart-series-area-line { - -fx-stroke: -bs-buy; + -fx-stroke: -bs-sell; + -fx-stroke-width: 2px; } /* The .chart-line-symbol rules change the color of the legend symbol */ @@ -1907,13 +2171,6 @@ textfield */ -fx-stroke-width: 2px; } -#charts .default-color0.chart-series-area-fill { - -fx-fill: -bs-sell-transparent; -} - -#charts .default-color1.chart-series-area-fill, #charts-dao .default-color0.chart-series-area-fill { - -fx-fill: -bs-buy-transparent; -} .chart-vertical-grid-lines { -fx-stroke: transparent; } @@ -2013,22 +2270,42 @@ textfield */ -fx-text-fill: -bs-rd-error-red; } -.popup-bg, .notification-popup-bg, .peer-info-popup-bg { +.popup-headline-information.label .glyph-icon, +.popup-headline-warning.label .glyph-icon, +.popup-icon-information.label .glyph-icon, +.popup-icon-warning.label .glyph-icon { + -fx-fill: -bs-color-primary; +} + +.popup-bg { -fx-font-size: 1.077em; - -fx-text-fill: -bs-rd-font-dark; - -fx-background-color: -bs-background-color; - -fx-background-radius: 0; + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); +} + +.notification-popup-bg, .peer-info-popup-bg { + -fx-font-size: 0.846em; + -fx-text-fill: -bs-rd-font-dark; + -fx-background-color: -bs-color-background-popup; + -fx-background-insets: 44; + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow-light-mode, 44, 0, 0, 0); + -fx-background-radius: 15; + -fx-border-radius: 15; } .popup-bg-top { -fx-font-size: 1.077em; -fx-text-fill: -bs-rd-font-dark; - -fx-background-color: -bs-background-color; - -fx-background-radius: 0; + -fx-background-color: -bs-color-background-popup-blur; -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, 0, 0); + -fx-background-radius: 0 0 15px 15px; +} + +.popup-dropshadow { + -fx-effect: dropshadow(gaussian, -bs-text-color-dropshadow, 20, 0, 0, 0); } .notification-popup-headline, peer-info-popup-headline { @@ -2037,18 +2314,6 @@ textfield */ -fx-text-fill: -bs-color-primary; } -.notification-popup-bg { - -fx-font-size: 0.846em; - -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); -} - -.peer-info-popup-bg { - -fx-font-size: 0.846em; - -fx-background-insets: 44; - -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); -} - .account-status-title { -fx-font-size: 0.769em; -fx-font-family: "IBM Plex Sans Medium"; @@ -2069,7 +2334,7 @@ textfield */ } #price-feed-combo > .list-cell { - -fx-text-fill: -bs-text-color; + -fx-text-fill: -bs-rd-font-balance; -fx-font-family: "IBM Plex Sans"; } @@ -2089,42 +2354,48 @@ textfield */ } #toggle-left { - -fx-border-radius: 4 0 0 4; -fx-border-color: -bs-rd-separator-dark; + -fx-border-radius: 4 0 0 4; -fx-border-style: solid; - -fx-border-width: 0 1 0 0; + -fx-border-width: 1 1 1 1; -fx-background-radius: 4 0 0 4; + -fx-border-insets: 0; + -fx-background-insets: 1 1 1 1; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } #toggle-center { - -fx-border-radius: 0; -fx-border-color: -bs-rd-separator-dark; + -fx-border-radius: 0; -fx-border-style: solid; - -fx-border-width: 0 1 0 0; + -fx-border-width: 1 1 1 0; -fx-border-insets: 0; - -fx-background-insets: 0; + -fx-background-insets: 1 1 1 0; -fx-background-radius: 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } -#toggle-center:selected, #toggle-left:selected, #toggle-right:selected { - -fx-text-fill: -bs-background-color; - -fx-background-color: -bs-toggle-selected; -} - #toggle-right { + -fx-border-color: -bs-rd-separator-dark; -fx-border-radius: 0 4 4 0; - -fx-border-width: 0; + -fx-border-width: 1 1 1 0; + -fx-border-insets: 0; + -fx-background-insets: 1 1 1 0; -fx-background-radius: 0 4 4 0; -fx-background-color: -bs-background-color; -fx-effect: dropshadow(gaussian, -bs-text-color-transparent, 4, 0, 0, 0, 2); } +#toggle-center:selected, #toggle-left:selected, #toggle-right:selected { + -fx-text-fill: white; + -fx-background-color: -bs-toggle-selected; +} + #toggle-left:hover, #toggle-right:hover, #toggle-center:hover { -fx-background-color: -bs-toggle-selected; + -fx-cursor: hand; } /******************************************************************************************************************** @@ -2136,10 +2407,18 @@ textfield */ -fx-text-fill: -bs-text-color; } +.message.label .glyph-icon { + -fx-fill: -bs-text-color; +} + .my-message { -fx-text-fill: -bs-background-color; } +.my-message.label .glyph-icon { + -fx-fill: -bs-background-color; +} + .message-header { -fx-text-fill: -bs-color-gray-3; -fx-font-size: 0.846em; @@ -2308,18 +2587,103 @@ textfield */ /******************************************************************************************************************** * * - * Popover * + * Popover * * * ********************************************************************************************************************/ .popover > .content { -fx-padding: 10; + -fx-background-color: -bs-color-background-popup; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-background-insets: 1; } .popover > .content .default-text { -fx-text-fill: -bs-text-color; } -.popover > .border { - -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-transparent-dark) !important; - -fx-fill: -bs-background-color !important; +.popover > .content .text-field { + -fx-background-color: -bs-color-background-form-field-readonly !important; + -fx-border-radius: 4; + -fx-background-radius: 4; +} + +.popover > .border { + -fx-stroke: linear-gradient(to bottom, -bs-text-color-transparent, -bs-text-color-dropshadow) !important; + -fx-fill: -bs-color-background-popup !important; +} + +/******************************************************************************************************************** + * * + * Other * + * * + ********************************************************************************************************************/ +.input-with-border { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-border-insets: 1 0 1 0; + -fx-background-insets: 1 0 1 0; +} + +.table-view.non-interactive-table .column-header .label { + -fx-text-fill: -bs-text-color-dim2; +} + +.highlight-text { + -fx-text-fill: -fx-dark-text-color !important; +} + +.grid-pane .text-area, +.flat-text-area-with-border { + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans"; + -fx-font-weight: normal; + -fx-text-fill: -bs-rd-font-dark-gray !important; + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field !important; +} + +.grid-pane .text-area:readonly, +.flat-text-area-with-border { + -fx-background-color: transparent !important; +} + +.grid-pane .text-area { + -fx-max-height: 150 !important; +} + +.passphrase-copy-box { + -fx-border-width: 1; + -fx-border-color: -bs-color-border-form-field; + -fx-background-radius: 8; + -fx-border-radius: 8; + -fx-padding: 13; + -fx-background-insets: 0; +} + +.passphrase-copy-box > .jfx-text-field { + -fx-padding: 0; + -fx-background-color: transparent; + -fx-border-width: 0; +} + +.passphrase-copy-box .label { + -fx-text-fill: white; + -fx-padding: 0; +} + +.passphrase-copy-box .jfx-button { + -fx-padding: 5 15 5 15; + -fx-background-radius: 999; + -fx-border-radius: 999; + -fx-min-height: 0; + -fx-font-size: 1.077em; + -fx-font-family: "IBM Plex Sans"; + -fx-font-weight: normal; +} + +.popup-with-input { + -fx-background-color: -bs-color-background-popup-input; } diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index aacd4c6b1b..e2a6cb84c2 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -1,16 +1,3 @@ -/* splash screen */ -/*noinspection CssUnknownTarget*/ -#image-splash-logo { - -fx-image: url("../../images/logo_splash.png"); -} - -/* splash screen testnet */ -/*noinspection CssUnknownTarget*/ -#image-splash-testnet-logo { - -fx-image: url("../../images/logo_splash_testnet.png"); -} - -/* shared*/ #image-info { -fx-image: url("../../images/info.png"); } @@ -23,16 +10,29 @@ -fx-image: url("../../images/alert_round.png"); } +#image-red_circle_solid { + -fx-image: url("../../images/red_circle_solid.png"); +} + + #image-green_circle { -fx-image: url("../../images/green_circle.png"); } +#image-green_circle_solid { + -fx-image: url("../../images/green_circle_solid.png"); +} + #image-yellow_circle { -fx-image: url("../../images/yellow_circle.png"); } -#image-blue_circle { - -fx-image: url("../../images/blue_circle.png"); +#image-yellow_circle_solid { + -fx-image: url("../../images/yellow_circle_solid.png"); +} + +#image-blue_circle_solid { + -fx-image: url("../../images/blue_circle_solid.png"); } #image-remove { @@ -300,3 +300,83 @@ #image-new-trade-protocol-screenshot { -fx-image: url("../../images/new_trade_protocol_screenshot.png"); } + +#image-support { + -fx-image: url("../../images/support.png"); +} + +#image-account { + -fx-image: url("../../images/account.png"); +} + +#image-settings { + -fx-image: url("../../images/settings.png"); +} + +#image-fiat-logo { + -fx-image: url("../../images/fiat_logo_light_mode.png"); +} + +#image-btc-logo { + -fx-image: url("../../images/btc_logo.png"); +} + +#image-bch-logo { + -fx-image: url("../../images/bch_logo.png"); +} + +#image-dai-erc20-logo { + -fx-image: url("../../images/dai-erc20_logo.png"); +} + +#image-eth-logo { + -fx-image: url("../../images/eth_logo.png"); +} + +#image-ltc-logo { + -fx-image: url("../../images/ltc_logo.png"); +} + +#image-usdc-erc20-logo { + -fx-image: url("../../images/usdc-erc20_logo.png"); +} + +#image-usdt-erc20-logo { + -fx-image: url("../../images/usdt-erc20_logo.png"); +} + +#image-usdt-trc20-logo { + -fx-image: url("../../images/usdt-trc20_logo.png"); +} + +#image-xmr-logo { + -fx-image: url("../../images/xmr_logo.png"); +} + +#image-xrp-logo { + -fx-image: url("../../images/xrp_logo.png"); +} + +#image-ada-logo { + -fx-image: url("../../images/ada_logo.png"); +} + +#image-sol-logo { + -fx-image: url("../../images/sol_logo.png"); +} + +#image-trx-logo { + -fx-image: url("../../images/trx_logo.png"); +} + +#image-doge-logo { + -fx-image: url("../../images/doge_logo.png"); +} + +#image-dark-mode-toggle { + -fx-image: url("../../images/dark_mode_toggle.png"); +} + +#image-light-mode-toggle { + -fx-image: url("../../images/light_mode_toggle.png"); +} diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index f294eea7bf..cedad685e1 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -32,6 +32,7 @@ import haveno.core.locale.GlobalSettings; import haveno.core.locale.LanguageUtil; import haveno.core.locale.Res; import haveno.core.provider.price.MarketPrice; +import haveno.core.user.Preferences; import haveno.desktop.Navigation; import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; @@ -73,6 +74,7 @@ import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Orientation; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -92,6 +94,7 @@ import static javafx.scene.layout.AnchorPane.setRightAnchor; import static javafx.scene.layout.AnchorPane.setTopAnchor; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -121,21 +124,23 @@ public class MainView extends InitializableView { private ChangeListener splashP2PNetworkVisibleListener; private BusyAnimation splashP2PNetworkBusyAnimation; private Label splashP2PNetworkLabel; - private ProgressBar xmrSyncIndicator, p2pNetworkProgressBar; + private ProgressBar xmrSyncIndicator; private Label xmrSplashInfo; private Popup p2PNetworkWarnMsgPopup, xmrNetworkWarnMsgPopup; private final TorNetworkSettingsWindow torNetworkSettingsWindow; + private final Preferences preferences; + private static final int networkIconSize = 20; public static StackPane getRootContainer() { return MainView.rootContainer; } public static void blurLight() { - transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 5); + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void blurUltraLight() { - transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 2); + transitions.blur(MainView.rootContainer, Transitions.DEFAULT_DURATION, -0.6, false, 15); } public static void darken() { @@ -151,12 +156,14 @@ public class MainView extends InitializableView { CachingViewLoader viewLoader, Navigation navigation, Transitions transitions, - TorNetworkSettingsWindow torNetworkSettingsWindow) { + TorNetworkSettingsWindow torNetworkSettingsWindow, + Preferences preferences) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; MainView.transitions = transitions; this.torNetworkSettingsWindow = torNetworkSettingsWindow; + this.preferences = preferences; } @Override @@ -165,15 +172,15 @@ public class MainView extends InitializableView { if (LanguageUtil.isDefaultLanguageRTL()) MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); - ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market").toUpperCase()); - ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr").toUpperCase()); - ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr").toUpperCase()); - ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio").toUpperCase()); - ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase()); + ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market")); + ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr")); + ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr")); + ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio")); + ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds")); - ToggleButton supportButton = new NavButton(SupportView.class, Res.get("mainView.menu.support")); - ToggleButton settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); - ToggleButton accountButton = new NavButton(AccountView.class, Res.get("mainView.menu.account")); + ToggleButton supportButton = new SecondaryNavButton(SupportView.class, Res.get("mainView.menu.support"), "image-support"); + ToggleButton accountButton = new SecondaryNavButton(AccountView.class, Res.get("mainView.menu.account"), "image-account"); + ToggleButton settingsButton = new SecondaryNavButton(SettingsView.class, Res.get("mainView.menu.settings"), "image-settings"); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); @@ -199,10 +206,10 @@ public class MainView extends InitializableView { fundsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT6, keyEvent)) { supportButton.fire(); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { - settingsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT8, keyEvent)) { accountButton.fire(); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { + settingsButton.fire(); } }); } @@ -298,47 +305,60 @@ public class MainView extends InitializableView { } }); - HBox primaryNav = new HBox(marketButton, getNavigationSeparator(), buyButton, getNavigationSeparator(), - sellButton, getNavigationSeparator(), portfolioButtonWithBadge, getNavigationSeparator(), fundsButton); + // add spacer to center the nav buttons when window is small + Region rightSpacer = new Region(); + HBox.setHgrow(rightSpacer, Priority.ALWAYS); + + HBox primaryNav = new HBox(getLogoPane(), marketButton, getNavigationSpacer(), buyButton, getNavigationSpacer(), + sellButton, getNavigationSpacer(), portfolioButtonWithBadge, getNavigationSpacer(), fundsButton, rightSpacer); primaryNav.setAlignment(Pos.CENTER_LEFT); primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); - HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButtonWithBadge, - getNavigationSpacer(), accountButton, getNavigationSpacer()); - secondaryNav.getStyleClass().add("nav-secondary"); - HBox.setHgrow(secondaryNav, Priority.SOMETIMES); - - secondaryNav.setAlignment(Pos.CENTER); - HBox priceAndBalance = new HBox(marketPriceBox.second, getNavigationSeparator(), availableBalanceBox.second, getNavigationSeparator(), pendingBalanceBox.second, getNavigationSeparator(), reservedBalanceBox.second); - priceAndBalance.setMaxHeight(41); priceAndBalance.setAlignment(Pos.CENTER); - priceAndBalance.setSpacing(9); + priceAndBalance.setSpacing(12); priceAndBalance.getStyleClass().add("nav-price-balance"); - HBox navPane = new HBox(primaryNav, secondaryNav, getNavigationSpacer(), - priceAndBalance) {{ - setLeftAnchor(this, 0d); - setRightAnchor(this, 0d); - setTopAnchor(this, 0d); + HBox navPane = new HBox(primaryNav, priceAndBalance) {{ + setLeftAnchor(this, 25d); + setRightAnchor(this, 25d); + setTopAnchor(this, 20d); setPadding(new Insets(0, 0, 0, 0)); getStyleClass().add("top-navigation"); }}; navPane.setAlignment(Pos.CENTER); + HBox secondaryNav = new HBox(supportButtonWithBadge, accountButton, settingsButtonWithBadge); + secondaryNav.getStyleClass().add("nav-secondary"); + secondaryNav.setAlignment(Pos.CENTER_RIGHT); + secondaryNav.setPickOnBounds(false); + HBox.setHgrow(secondaryNav, Priority.ALWAYS); + AnchorPane.setLeftAnchor(secondaryNav, 0.0); + AnchorPane.setRightAnchor(secondaryNav, 0.0); + AnchorPane.setTopAnchor(secondaryNav, 0.0); + + AnchorPane secondaryNavContainer = new AnchorPane() {{ + setId("nav-secondary-container"); + setLeftAnchor(this, 0d); + setRightAnchor(this, 0d); + setTopAnchor(this, 94d); + }}; + secondaryNavContainer.setPickOnBounds(false); + secondaryNavContainer.getChildren().add(secondaryNav); + AnchorPane contentContainer = new AnchorPane() {{ getStyleClass().add("content-pane"); setLeftAnchor(this, 0d); setRightAnchor(this, 0d); - setTopAnchor(this, 57d); + setTopAnchor(this, 95d); setBottomAnchor(this, 0d); }}; - AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer) {{ + AnchorPane applicationContainer = new AnchorPane(navPane, contentContainer, secondaryNavContainer) {{ setId("application-container"); }}; @@ -353,7 +373,7 @@ public class MainView extends InitializableView { settingsButtonWithBadge.getStyleClass().add("new"); navigation.addListener((viewPath, data) -> { - UserThread.await(() -> { + UserThread.await(() -> { // TODO: this uses `await` to fix nagivation link from market view to offer book, but await can cause hanging, so execute should be used if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) return; Class viewClass = viewPath.tip(); @@ -398,15 +418,32 @@ public class MainView extends InitializableView { private Separator getNavigationSeparator() { final Separator separator = new Separator(Orientation.VERTICAL); HBox.setHgrow(separator, Priority.ALWAYS); - separator.setMaxHeight(22); separator.setMaxWidth(Double.MAX_VALUE); + separator.getStyleClass().add("nav-separator"); return separator; } + @NotNull + private Pane getLogoPane() { + ImageView logo = new ImageView(); + logo.setId("image-logo-landscape"); + logo.setPreserveRatio(true); + logo.setFitHeight(40); + logo.setSmooth(true); + logo.setCache(true); + + final Pane pane = new Pane(); + HBox.setHgrow(pane, Priority.ALWAYS); + pane.getStyleClass().add("nav-logo"); + pane.getChildren().add(logo); + return pane; + } + @NotNull private Region getNavigationSpacer() { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); + spacer.getStyleClass().add("nav-spacer"); return spacer; } @@ -447,7 +484,6 @@ public class MainView extends InitializableView { priceComboBox.setVisibleRowCount(12); priceComboBox.setFocusTraversable(false); priceComboBox.setId("price-feed-combo"); - priceComboBox.setPadding(new Insets(0, -4, -4, 0)); priceComboBox.setCellFactory(p -> getPriceFeedComboBoxListCell()); ListCell buttonCell = getPriceFeedComboBoxListCell(); buttonCell.setId("price-feed-combo"); @@ -458,7 +494,6 @@ public class MainView extends InitializableView { updateMarketPriceLabel(marketPriceLabel); marketPriceLabel.getStyleClass().add("nav-balance-label"); - marketPriceLabel.setPadding(new Insets(-2, 0, 4, 9)); marketPriceBox.getChildren().addAll(priceComboBox, marketPriceLabel); @@ -509,14 +544,24 @@ public class MainView extends InitializableView { vBox.setId("splash"); ImageView logo = new ImageView(); - logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-splash-logo" : "image-splash-testnet-logo"); + logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-logo-splash" : "image-logo-splash-testnet"); + logo.setFitWidth(400); + logo.setPreserveRatio(true); + logo.setSmooth(true); // createBitcoinInfoBox xmrSplashInfo = new AutoTooltipLabel(); xmrSplashInfo.textProperty().bind(model.getXmrInfo()); walletServiceErrorMsgListener = (ov, oldValue, newValue) -> { - xmrSplashInfo.setId("splash-error-state-msg"); - xmrSplashInfo.getStyleClass().add("error-text"); + UserThread.execute(() -> { + if (newValue != null && !newValue.isEmpty()) { + xmrSplashInfo.setId("splash-error-state-msg"); + if (!xmrSplashInfo.getStyleClass().contains("error-text")) xmrSplashInfo.getStyleClass().add("error-text"); + } else { + xmrSplashInfo.setId(""); + xmrSplashInfo.getStyleClass().remove("error-text"); + } + }); }; model.getConnectionServiceErrorMsg().addListener(walletServiceErrorMsgListener); @@ -552,7 +597,7 @@ public class MainView extends InitializableView { // create P2PNetworkBox splashP2PNetworkLabel = new AutoTooltipLabel(); splashP2PNetworkLabel.setWrapText(true); - splashP2PNetworkLabel.setMaxWidth(500); + splashP2PNetworkLabel.setMaxWidth(700); splashP2PNetworkLabel.setTextAlignment(TextAlignment.CENTER); splashP2PNetworkLabel.getStyleClass().add("sub-info"); splashP2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); @@ -587,9 +632,11 @@ public class MainView extends InitializableView { ImageView splashP2PNetworkIcon = new ImageView(); splashP2PNetworkIcon.setId("image-connection-tor"); + splashP2PNetworkIcon.setFitWidth(networkIconSize); + splashP2PNetworkIcon.setFitHeight(networkIconSize); splashP2PNetworkIcon.setVisible(false); splashP2PNetworkIcon.setManaged(false); - HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0)); + HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 0, 0)); splashP2PNetworkIcon.setOnMouseClicked(e -> { torNetworkSettingsWindow.show(); }); @@ -603,6 +650,8 @@ public class MainView extends InitializableView { splashP2PNetworkIcon.setId(newValue); splashP2PNetworkIcon.setVisible(true); splashP2PNetworkIcon.setManaged(true); + splashP2PNetworkIcon.setFitWidth(networkIconSize); + splashP2PNetworkIcon.setFitHeight(networkIconSize); // if we can connect in 10 sec. we know that tor is working showTorNetworkSettingsTimer.stop(); @@ -668,19 +717,19 @@ public class MainView extends InitializableView { //blockchainSyncIndicator.progressProperty().bind(model.getCombinedSyncProgress()); model.getConnectionServiceErrorMsg().addListener((ov, oldValue, newValue) -> { - if (newValue != null) { - xmrInfoLabel.setId("splash-error-state-msg"); - xmrInfoLabel.getStyleClass().add("error-text"); - if (xmrNetworkWarnMsgPopup == null) { + UserThread.execute(() -> { + if (newValue != null && !newValue.isEmpty()) { + xmrInfoLabel.setId("splash-error-state-msg"); + if (!xmrInfoLabel.getStyleClass().contains("error-text")) xmrInfoLabel.getStyleClass().add("error-text"); xmrNetworkWarnMsgPopup = new Popup().warning(newValue); xmrNetworkWarnMsgPopup.show(); + } else { + xmrInfoLabel.setId("footer-pane"); + xmrInfoLabel.getStyleClass().remove("error-text"); + if (xmrNetworkWarnMsgPopup != null) + xmrNetworkWarnMsgPopup.hide(); } - } else { - xmrInfoLabel.setId("footer-pane"); - xmrInfoLabel.getStyleClass().remove("error-text"); - if (xmrNetworkWarnMsgPopup != null) - xmrNetworkWarnMsgPopup.hide(); - } + }); }); model.getTopErrorMsg().addListener((ov, oldValue, newValue) -> { @@ -725,15 +774,39 @@ public class MainView extends InitializableView { setRightAnchor(versionBox, 10d); setBottomAnchor(versionBox, 7d); + // Dark mode toggle + ImageView useDarkModeIcon = new ImageView(); + useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); + useDarkModeIcon.setFitHeight(networkIconSize); + useDarkModeIcon.setPreserveRatio(true); + useDarkModeIcon.setPickOnBounds(true); + useDarkModeIcon.setCursor(Cursor.HAND); + setRightAnchor(useDarkModeIcon, 8d); + setBottomAnchor(useDarkModeIcon, 6d); + Tooltip modeToolTip = new Tooltip(); + Tooltip.install(useDarkModeIcon, modeToolTip); + useDarkModeIcon.setOnMouseEntered(e -> modeToolTip.setText(Res.get(preferences.getCssTheme() == 1 ? "setting.preferences.useLightMode" : "setting.preferences.useDarkMode"))); + useDarkModeIcon.setOnMouseClicked(e -> { + preferences.setCssTheme(preferences.getCssTheme() != 1); + }); + preferences.getCssThemeProperty().addListener((observable, oldValue, newValue) -> { + useDarkModeIcon.setId(preferences.getCssTheme() == 1 ? "image-dark-mode-toggle" : "image-light-mode-toggle"); + }); + // P2P Network Label p2PNetworkLabel = new AutoTooltipLabel(); p2PNetworkLabel.setId("footer-pane"); p2PNetworkLabel.textProperty().bind(model.getP2PNetworkInfo()); + double networkIconRightAnchor = 54d; ImageView p2PNetworkIcon = new ImageView(); - setRightAnchor(p2PNetworkIcon, 10d); - setBottomAnchor(p2PNetworkIcon, 5d); + setRightAnchor(p2PNetworkIcon, networkIconRightAnchor); + setBottomAnchor(p2PNetworkIcon, 6d); + p2PNetworkIcon.setPickOnBounds(true); + p2PNetworkIcon.setCursor(Cursor.HAND); p2PNetworkIcon.setOpacity(0.4); + p2PNetworkIcon.setFitWidth(networkIconSize); + p2PNetworkIcon.setFitHeight(networkIconSize); p2PNetworkIcon.idProperty().bind(model.getP2PNetworkIconId()); p2PNetworkLabel.idProperty().bind(model.getP2pNetworkLabelId()); model.getP2pNetworkWarnMsg().addListener((ov, oldValue, newValue) -> { @@ -749,8 +822,12 @@ public class MainView extends InitializableView { }); ImageView p2PNetworkStatusIcon = new ImageView(); - setRightAnchor(p2PNetworkStatusIcon, 30d); - setBottomAnchor(p2PNetworkStatusIcon, 7d); + p2PNetworkStatusIcon.setPickOnBounds(true); + p2PNetworkStatusIcon.setCursor(Cursor.HAND); + p2PNetworkStatusIcon.setFitWidth(networkIconSize); + p2PNetworkStatusIcon.setFitHeight(networkIconSize); + setRightAnchor(p2PNetworkStatusIcon, networkIconRightAnchor + 22); + setBottomAnchor(p2PNetworkStatusIcon, 6d); Tooltip p2pNetworkStatusToolTip = new Tooltip(); Tooltip.install(p2PNetworkStatusIcon, p2pNetworkStatusToolTip); p2PNetworkStatusIcon.setOnMouseEntered(e -> p2pNetworkStatusToolTip.setText(model.getP2pConnectionSummary())); @@ -781,20 +858,15 @@ public class MainView extends InitializableView { model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> UserThread.execute(() -> { p2PNetworkIcon.setOpacity(1); - p2pNetworkProgressBar.setProgress(0); })); - p2pNetworkProgressBar = new ProgressBar(-1); - p2pNetworkProgressBar.setMaxHeight(2); - p2pNetworkProgressBar.prefWidthProperty().bind(p2PNetworkLabel.widthProperty()); - VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); - vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); - setRightAnchor(vBox, 53d); - setBottomAnchor(vBox, 5d); + vBox.getChildren().addAll(p2PNetworkLabel); + setRightAnchor(vBox, networkIconRightAnchor + 45); + setBottomAnchor(vBox, 7d); - return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon) {{ + return new AnchorPane(separator, xmrInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon, useDarkModeIcon) {{ setId("footer-pane"); setMinHeight(30); setMaxHeight(30); @@ -825,6 +897,9 @@ public class MainView extends InitializableView { this.setToggleGroup(navButtons); this.getStyleClass().add("nav-button"); + this.setMinWidth(Region.USE_PREF_SIZE); // prevent squashing content + this.setPrefWidth(Region.USE_COMPUTED_SIZE); + // Japanese fonts are dense, increase top nav button text size if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { this.getStyleClass().add("nav-button-japanese"); @@ -836,4 +911,29 @@ public class MainView extends InitializableView { } } + + private class SecondaryNavButton extends NavButton { + + SecondaryNavButton(Class viewClass, String title, String iconId) { + super(viewClass, title); + this.getStyleClass().setAll("nav-secondary-button"); + + // Japanese fonts are dense, increase top nav button text size + if (model.getPreferences() != null && "ja".equals(model.getPreferences().getUserLanguage())) { + this.getStyleClass().setAll("nav-secondary-button-japanese"); + } + + // add icon + ImageView imageView = new ImageView(); + imageView.setId(iconId); + imageView.setFitWidth(15); + imageView.setPreserveRatio(true); + setGraphicTextGap(10); + setGraphic(imageView); + + // show cursor hand on any hover + this.setPickOnBounds(true); + } + + } } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 16cef449d6..473b18d331 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -18,6 +18,8 @@ package haveno.desktop.main; import com.google.inject.Inject; + +import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.app.DevEnv; @@ -215,51 +217,53 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @Override public void onSetupComplete() { // We handle the trade period here as we display a global popup if we reached dispute time - tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.persistedTradesInitializedProperty(), + tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.tradesInitializedProperty(), (a, b) -> a && b); tradesAndUIReady.subscribe((observable, oldValue, newValue) -> { if (newValue) { - tradeManager.applyTradePeriodState(); + ThreadUtils.submitToPool(() -> { + tradeManager.applyTradePeriodState(); - tradeManager.getOpenTrades().forEach(trade -> { + tradeManager.getOpenTrades().forEach(trade -> { - // check initialization error - if (trade.getInitError() != null) { - new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" + - trade.getInitError().getMessage()) - .show(); - return; - } + // check initialization error + if (trade.getInitError() != null) { + new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" + + trade.getInitError().getMessage()) + .show(); + return; + } - // check trade period - Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); - String key; - switch (trade.getPeriodState()) { - case FIRST_HALF: - break; - case SECOND_HALF: - key = "displayHalfTradePeriodOver" + trade.getId(); - if (DontShowAgainLookup.showAgain(key)) { - DontShowAgainLookup.dontShowAgain(key, true); - if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade - new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached", - trade.getShortId(), - DisplayUtils.formatDateTime(maxTradePeriodDate))) - .show(); - } - break; - case TRADE_PERIOD_OVER: - key = "displayTradePeriodOver" + trade.getId(); - if (DontShowAgainLookup.showAgain(key)) { - DontShowAgainLookup.dontShowAgain(key, true); - if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade - new Popup().warning(Res.get("popup.warning.tradePeriod.ended", - trade.getShortId(), - DisplayUtils.formatDateTime(maxTradePeriodDate))) - .show(); - } - break; - } + // check trade period + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + String key; + switch (trade.getPeriodState()) { + case FIRST_HALF: + break; + case SECOND_HALF: + key = "displayHalfTradePeriodOver" + trade.getId(); + if (DontShowAgainLookup.showAgain(key)) { + DontShowAgainLookup.dontShowAgain(key, true); + if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade + new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached", + trade.getShortId(), + DisplayUtils.formatDateTime(maxTradePeriodDate))) + .show(); + } + break; + case TRADE_PERIOD_OVER: + key = "displayTradePeriodOver" + trade.getId(); + if (DontShowAgainLookup.showAgain(key)) { + DontShowAgainLookup.dontShowAgain(key, true); + if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade + new Popup().warning(Res.get("popup.warning.tradePeriod.ended", + trade.getShortId(), + DisplayUtils.formatDateTime(maxTradePeriodDate))) + .show(); + } + break; + } + }); }); } }); @@ -337,7 +341,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { + havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> { if (connectionError == null) { if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); } else { @@ -349,7 +353,6 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .actionButtonText(Res.get("xmrConnectionError.localNode.start")) .onAction(() -> { log.warn("User has chosen to start local node."); - havenoSetup.getConnectionServiceError().set(null); new Thread(() -> { try { HavenoUtils.xmrConnectionService.startLocalNode(); @@ -359,16 +362,20 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .headLine(Res.get("xmrConnectionError.localNode.start.error")) .warning(e.getMessage()) .closeButtonText(Res.get("shared.close")) - .onClose(() -> havenoSetup.getConnectionServiceError().set(null)) + .onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null)) .show(); + } finally { + havenoSetup.getConnectionServiceFallbackType().set(null); } }).start(); }) .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) .onSecondaryAction(() -> { log.warn("User has chosen to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.shutDown")) .onClose(HavenoApp.getShutDownHandler()); @@ -376,16 +383,35 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener case CUSTOM: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) - .warning(Res.get("xmrConnectionError.customNode")) + .warning(Res.get("xmrConnectionError.customNodes")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.no")) .onClose(() -> { log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); + havenoSetup.getConnectionServiceFallbackType().set(null); + }); + break; + case PROVIDED: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.providedNodes")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); + }) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackType().set(null); }); break; } diff --git a/desktop/src/main/java/haveno/desktop/main/account/AccountView.java b/desktop/src/main/java/haveno/desktop/main/account/AccountView.java index 30c434cccd..028276a83a 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/AccountView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/AccountView.java @@ -86,12 +86,12 @@ public class AccountView extends ActivatableView { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); - traditionalAccountsTab.setText(Res.get("account.menu.paymentAccount").toUpperCase()); - cryptoAccountsTab.setText(Res.get("account.menu.altCoinsAccountView").toUpperCase()); - passwordTab.setText(Res.get("account.menu.password").toUpperCase()); - seedWordsTab.setText(Res.get("account.menu.seedWords").toUpperCase()); - //walletInfoTab.setText(Res.get("account.menu.walletInfo").toUpperCase()); - backupTab.setText(Res.get("account.menu.backup").toUpperCase()); + traditionalAccountsTab.setText(Res.get("account.menu.paymentAccount")); + cryptoAccountsTab.setText(Res.get("account.menu.altCoinsAccountView")); + passwordTab.setText(Res.get("account.menu.password")); + seedWordsTab.setText(Res.get("account.menu.seedWords")); + //walletInfoTab.setText(Res.get("account.menu.walletInfo")); + backupTab.setText(Res.get("account.menu.backup")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) { diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java index cd22e98a3f..cd89273c9b 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/PaymentAccountsView.java @@ -14,7 +14,9 @@ import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.ImageUtil; +import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.scene.Node; @@ -67,6 +69,10 @@ public abstract class PaymentAccountsView) change -> { + setPaymentAccountsListHeight(); + }); } @Override @@ -153,6 +159,13 @@ public abstract class PaymentAccountsView { Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("account.backup.selectLocation"), Res.get("account.backup.backupNow")); selectBackupDir = tuple2.first; + selectBackupDir.setId("buy-button-big"); backupNow = tuple2.second; updateButtons(); diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java index 8abcd98a9d..d0773f5961 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsView.java @@ -174,13 +174,13 @@ public class CryptoAccountsView extends PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.crypto.yourCryptoAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; - int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; + addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } @@ -190,7 +190,7 @@ public class CryptoAccountsView extends PaymentAccountsView private void addContent() { TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++rowIndex); GridPane.setColumnSpan(tableView, 2); GridPane.setMargin(tableView, new Insets(10, 0, 0, 0)); diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java index a5f7332c81..b6e8a575bc 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsView.java @@ -455,13 +455,13 @@ public class TraditionalAccountsView extends PaymentAccountsView, VBox> tuple = addTopLabelListView(root, gridRow, Res.get("account.traditional.yourTraditionalAccounts"), Layout.FIRST_ROW_DISTANCE); paymentAccountsListView = tuple.second; - int prefNumRows = Math.min(4, Math.max(2, model.dataModel.getNumPaymentAccounts())); - paymentAccountsListView.setMinHeight(prefNumRows * Layout.LIST_ROW_HEIGHT + 28); + setPaymentAccountsListHeight(); setPaymentAccountsCellFactory(); Tuple3 tuple3 = add3ButtonsAfterGroup(root, ++gridRow, Res.get("shared.addNewAccount"), Res.get("shared.ExportAccounts"), Res.get("shared.importAccounts")); addAccountButton = tuple3.first; + addAccountButton.setId("buy-button-big"); exportButton = tuple3.second; importButton = tuple3.third; } @@ -472,10 +472,10 @@ public class TraditionalAccountsView extends PaymentAccountsView { + if (paymentMethodComboBox.getEditor().getText().isEmpty()) + return; if (paymentMethodForm != null) { FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); @@ -535,7 +537,7 @@ public class TraditionalAccountsView extends PaymentAccountsView { @Override public void initialize() { - depositTab.setText(Res.get("funds.tab.deposit").toUpperCase()); - withdrawalTab.setText(Res.get("funds.tab.withdrawal").toUpperCase()); - transactionsTab.setText(Res.get("funds.tab.transactions").toUpperCase()); + depositTab.setText(Res.get("funds.tab.deposit")); + withdrawalTab.setText(Res.get("funds.tab.withdrawal")); + transactionsTab.setText(Res.get("funds.tab.transactions")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(FundsView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index 884df454e7..72f7945f65 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -40,6 +40,7 @@ import com.google.inject.name.Named; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; +import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; @@ -89,10 +90,9 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Callback; -import monero.common.MoneroUtils; -import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroWalletListener; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; @@ -111,6 +111,7 @@ public class DepositView extends ActivatableView { @FXML TableColumn addressColumn, balanceColumn, confirmationsColumn, usageColumn; private ImageView qrCodeImageView; + private StackPane qrCodePane; private AddressTextField addressTextField; private Button generateNewAddressButton; private TitledGroupBg titledGroupBg; @@ -144,6 +145,7 @@ public class DepositView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(tableView); paymentLabelString = Res.get("funds.deposit.fundHavenoWallet"); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); @@ -154,6 +156,7 @@ public class DepositView extends ActivatableView { // set loading placeholder Label placeholderLabel = new Label("Loading..."); tableView.setPlaceholder(placeholderLabel); + tableView.getStyleClass().add("non-interactive-table"); ThreadUtils.execute(() -> { @@ -190,19 +193,19 @@ public class DepositView extends ActivatableView { titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet")); titledGroupBg.getStyleClass().add("last"); - qrCodeImageView = new ImageView(); - qrCodeImageView.setFitHeight(150); - qrCodeImageView.setFitWidth(150); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getPaymentUri()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setRowSpan(qrCodeImageView, 4); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setRowSpan(qrCodePane, 4); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE); addressTextField.setPaymentLabel(paymentLabelString); @@ -213,8 +216,8 @@ public class DepositView extends ActivatableView { titledGroupBg.setVisible(false); titledGroupBg.setManaged(false); - qrCodeImageView.setVisible(false); - qrCodeImageView.setManaged(false); + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); amountTextField.setManaged(false); @@ -310,8 +313,8 @@ public class DepositView extends ActivatableView { private void fillForm(String address) { titledGroupBg.setVisible(true); titledGroupBg.setManaged(true); - qrCodeImageView.setVisible(true); - qrCodeImageView.setManaged(true); + qrCodePane.setVisible(true); + qrCodePane.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); amountTextField.setManaged(true); @@ -366,10 +369,7 @@ public class DepositView extends ActivatableView { @NotNull private String getPaymentUri() { - return MoneroUtils.getPaymentUri(new MoneroTxConfig() - .setAddress(addressTextField.getAddress()) - .setAmount(HavenoUtils.coinToAtomicUnits(getAmount())) - .setNote(paymentLabelString)); + return GUIUtil.getMoneroURI(addressTextField.getAddress(), HavenoUtils.coinToAtomicUnits(getAmount()), paymentLabelString); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -377,7 +377,6 @@ public class DepositView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setUsageColumnCellFactory() { - usageColumn.getStyleClass().add("last-column"); usageColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); usageColumn.setCellFactory(new Callback<>() { @@ -390,7 +389,9 @@ public class DepositView extends ActivatableView { public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - setGraphic(new AutoTooltipLabel(item.getUsage())); + Label usageLabel = new AutoTooltipLabel(item.getUsage()); + usageLabel.getStyleClass().add("highlight-text"); + setGraphic(usageLabel); } else { setGraphic(null); } @@ -401,7 +402,6 @@ public class DepositView extends ActivatableView { } private void setAddressColumnCellFactory() { - addressColumn.getStyleClass().add("first-column"); addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( @@ -434,6 +434,7 @@ public class DepositView extends ActivatableView { private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + balanceColumn.getStyleClass().add("highlight-text"); balanceColumn.setCellFactory(new Callback<>() { @Override diff --git a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java index 8cf5700cc6..8cb80b16f7 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java @@ -122,6 +122,7 @@ public class LockedView extends ActivatableView { addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); + GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.locked.noFunds"))); @@ -164,7 +165,7 @@ public class LockedView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - ObservableList> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; @@ -250,7 +251,6 @@ public class LockedView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @@ -342,7 +342,6 @@ public class LockedView extends ActivatableView { } private void setBalanceColumnCellFactory() { - balanceColumn.getStyleClass().add("last-column"); balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java index bcef7e6488..e71fae8d89 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java @@ -122,6 +122,7 @@ public class ReservedView extends ActivatableView { addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); balanceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.balanceWithCur", Res.getBaseCurrencyCode()))); + GUIUtil.applyTableStyle(tableView); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.reserved.noFunds"))); @@ -164,7 +165,7 @@ public class ReservedView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - ObservableList> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; @@ -249,7 +250,6 @@ public class ReservedView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setCellFactory(new Callback<>() { @@ -313,7 +313,6 @@ public class ReservedView extends ActivatableView { } private void setAddressColumnCellFactory() { - addressColumn.getStyleClass().add("last-column"); addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( @@ -341,7 +340,6 @@ public class ReservedView extends ActivatableView { } private void setBalanceColumnCellFactory() { - balanceColumn.getStyleClass().add("last-column"); balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java index b325b04efa..930d8f9572 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java @@ -22,6 +22,7 @@ import com.google.common.base.Suppliers; import haveno.core.locale.Res; import haveno.core.offer.Offer; import haveno.core.offer.OpenOffer; +import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Tradable; import haveno.core.trade.Trade; @@ -159,13 +160,16 @@ public class TransactionsListItem { } } else { details = Res.get("funds.tx.unknown", tradeId); + if (trade instanceof ArbitratorTrade) { + if (txId.equals(trade.getMaker().getDepositTxHash())) { + details = Res.get("funds.tx.makerTradeFee", tradeId); + } else if (txId.equals(trade.getTaker().getDepositTxHash())) { + details = Res.get("funds.tx.takerTradeFee", tradeId); + } + } } } } - } else { - if (amount.compareTo(BigInteger.ZERO) == 0) { - details = Res.get("funds.tx.noFundsFromDispute"); - } } // get tx date/time diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml index 24d8f121d7..15d2177129 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml @@ -32,15 +32,14 @@ - - - + + + - - - + + diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index b882782212..8e1e999f30 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -20,7 +20,6 @@ package haveno.desktop.main.funds.transactions; import com.google.inject.Inject; import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.fontawesome.AwesomeIcon; -import haveno.common.util.Utilities; import haveno.core.api.XmrConnectionService; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; @@ -47,13 +46,11 @@ import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Scene; -import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -70,7 +67,7 @@ public class TransactionsView extends ActivatableView { @FXML TableView tableView; @FXML - TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn, revertTxColumn; + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn; @FXML Label numItems; @FXML @@ -127,6 +124,8 @@ public class TransactionsView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(tableView); + dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); detailsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.details"))); addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); @@ -135,10 +134,10 @@ public class TransactionsView extends ActivatableView { txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode()))); memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); - revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode()))); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.tx.noTxAvailable"))); + tableView.getStyleClass().add("non-interactive-table"); setDateColumnCellFactory(); setDetailsColumnCellFactory(); @@ -148,7 +147,6 @@ public class TransactionsView extends ActivatableView { setTxFeeColumnCellFactory(); setConfidenceColumnCellFactory(); setMemoColumnCellFactory(); - setRevertTxColumnCellFactory(); dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate)); detailsColumn.setComparator((o1, o2) -> { @@ -167,15 +165,7 @@ public class TransactionsView extends ActivatableView { tableView.getSortOrder().add(dateColumn); keyEventEventHandler = event -> { - // Not intended to be public to users as the feature is not well tested - if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { - if (revertTxColumn.isVisible()) { - confidenceColumn.getStyleClass().remove("last-column"); - } else { - confidenceColumn.getStyleClass().add("last-column"); - } - revertTxColumn.setVisible(!revertTxColumn.isVisible()); - } + // unused }; HBox.setHgrow(spacer, Priority.ALWAYS); @@ -205,8 +195,8 @@ public class TransactionsView extends ActivatableView { numItems.setText(Res.get("shared.numItemsLabel", sortedDisplayedTransactions.size())); exportButton.setOnAction(event -> { - final ObservableList> tableColumns = tableView.getColumns(); - final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) + final ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); + final int reportColumns = tableColumns.size(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) @@ -265,7 +255,6 @@ public class TransactionsView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void setDateColumnCellFactory() { - dateColumn.getStyleClass().add("first-column"); dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); dateColumn.setMaxWidth(200); @@ -400,6 +389,7 @@ public class TransactionsView extends ActivatableView { private void setAmountColumnCellFactory() { amountColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + amountColumn.getStyleClass().add("highlight-text"); amountColumn.setCellFactory( new Callback<>() { @@ -427,6 +417,7 @@ public class TransactionsView extends ActivatableView { private void setTxFeeColumnCellFactory() { txFeeColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + txFeeColumn.getStyleClass().add("highlight-text"); txFeeColumn.setCellFactory( new Callback<>() { @@ -453,6 +444,7 @@ public class TransactionsView extends ActivatableView { private void setMemoColumnCellFactory() { memoColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + memoColumn.getStyleClass().add("highlight-text"); memoColumn.setCellFactory( new Callback<>() { @@ -477,7 +469,6 @@ public class TransactionsView extends ActivatableView { } private void setConfidenceColumnCellFactory() { - confidenceColumn.getStyleClass().add("last-column"); confidenceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); confidenceColumn.setCellFactory( @@ -502,32 +493,5 @@ public class TransactionsView extends ActivatableView { } }); } - - private void setRevertTxColumnCellFactory() { - revertTxColumn.getStyleClass().add("last-column"); - revertTxColumn.setCellValueFactory((addressListItem) -> - new ReadOnlyObjectWrapper<>(addressListItem.getValue())); - revertTxColumn.setCellFactory( - new Callback<>() { - - @Override - public TableCell call(TableColumn column) { - return new TableCell<>() { - Button button; - - @Override - public void updateItem(final TransactionsListItem item, boolean empty) { - super.updateItem(item, empty); - setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } - } - }; - } - }); - } } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 730b47adf6..64ab188454 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -40,7 +40,6 @@ import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.trade.protocol.TradeProtocol; -import haveno.core.user.DontShowAgainLookup; import haveno.core.util.validation.BtcAddressValidator; import haveno.core.xmr.listeners.XmrBalanceListener; import haveno.core.xmr.setup.WalletsSetup; @@ -144,7 +143,7 @@ public class WithdrawalView extends ActivatableView { amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; - amountTextField.setMinWidth(180); + amountTextField.setMinWidth(225); HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, @@ -330,12 +329,8 @@ public class WithdrawalView extends ActivatableView { try { xmrWalletService.getWallet().relayTx(tx); xmrWalletService.getWallet().setTxNote(tx.getHash(), withdrawMemoTextField.getText()); // TODO (monero-java): tx note does not persist when tx created then relayed - String key = "showTransactionSent"; - if (DontShowAgainLookup.showAgain(key)) { - new TxWithdrawWindow(tx.getHash(), withdrawToAddress, HavenoUtils.formatXmr(receiverAmount, true), HavenoUtils.formatXmr(fee, true), xmrWalletService.getWallet().getTxNote(tx.getHash())) - .dontShowAgainId(key) - .show(); - } + new TxWithdrawWindow(tx.getHash(), withdrawToAddress, HavenoUtils.formatXmr(receiverAmount, true), HavenoUtils.formatXmr(fee, true), xmrWalletService.getWallet().getTxNote(tx.getHash())) + .show(); log.debug("onWithdraw onSuccess tx ID:{}", tx.getHash()); } catch (Exception e) { e.printStackTrace(); diff --git a/desktop/src/main/java/haveno/desktop/main/market/MarketView.java b/desktop/src/main/java/haveno/desktop/main/market/MarketView.java index 98f5e75490..b4f5c02d0a 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/MarketView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/MarketView.java @@ -88,10 +88,10 @@ public class MarketView extends ActivatableView { @Override public void initialize() { - offerBookTab.setText(Res.get("market.tabs.offerBook").toUpperCase()); - spreadTab.setText(Res.get("market.tabs.spreadCurrency").toUpperCase()); - spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment").toUpperCase()); - tradesTab.setText(Res.get("market.tabs.trades").toUpperCase()); + offerBookTab.setText(Res.get("market.tabs.offerBook")); + spreadTab.setText(Res.get("market.tabs.spreadCurrency")); + spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment")); + tradesTab.setText(Res.get("market.tabs.trades")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(MarketView.class) == 1) @@ -209,7 +209,7 @@ public class MarketView extends ActivatableView { StringBuilder sb = new StringBuilder(); sb.append("Offer ID: ").append(offer.getId()).append("\n") .append("Type: ").append(offer.getDirection().name()).append("\n") - .append("Market: ").append(CurrencyUtil.getCurrencyPair(offer.getCurrencyCode())).append("\n") + .append("Market: ").append(CurrencyUtil.getCurrencyPair(offer.getCounterCurrencyCode())).append("\n") .append("Price: ").append(FormattingUtils.formatPrice(offer.getPrice())).append("\n") .append("Amount: ").append(DisplayUtils.formatAmount(offer, formatter)).append(" BTC\n") .append("Payment method: ").append(Res.get(offer.getPaymentMethod().getId())).append("\n") diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java index 3d2ec6d884..163337a448 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java @@ -26,6 +26,7 @@ import haveno.common.util.Tuple3; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; +import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.util.FormattingUtils; @@ -65,13 +66,13 @@ import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.SingleSelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -95,7 +96,10 @@ public class OfferBookChartView extends ActivatableViewAndModel currencyComboBox; private Subscription tradeCurrencySubscriber; - private final StringProperty volumeColumnLabel = new SimpleStringProperty(); + private final StringProperty volumeSellColumnLabel = new SimpleStringProperty(); + private final StringProperty volumeBuyColumnLabel = new SimpleStringProperty(); + private final StringProperty amountSellColumnLabel = new SimpleStringProperty(); + private final StringProperty amountBuyColumnLabel = new SimpleStringProperty(); private final StringProperty priceColumnLabel = new SimpleStringProperty(); private AutoTooltipButton sellButton; private AutoTooltipButton buyButton; @@ -106,10 +110,11 @@ public class OfferBookChartView extends ActivatableViewAndModel changeListener; private ListChangeListener currencyListItemsListener; private final double dataLimitFactor = 3; - private final double initialOfferTableViewHeight = 121; + private final double initialOfferTableViewHeight = 78; // decrease as MainView's content-pane's top anchor increases + private final double offerTableExtraMarginBottom = 0; private final Function offerTableViewHeight = (screenSize) -> { // initial visible row count=5, header height=30 - double pixelsPerOfferTableRow = (initialOfferTableViewHeight - 30) / 5.0; + double pixelsPerOfferTableRow = (initialOfferTableViewHeight - offerTableExtraMarginBottom) / 5.0; int extraRows = screenSize <= INITIAL_WINDOW_HEIGHT ? 0 : (int) ((screenSize - INITIAL_WINDOW_HEIGHT) / pixelsPerOfferTableRow); return extraRows == 0 ? initialOfferTableViewHeight : Math.ceil(initialOfferTableViewHeight + ((extraRows + 1) * pixelsPerOfferTableRow)); }; @@ -136,6 +141,7 @@ public class OfferBookChartView extends ActivatableViewAndModel { String code = tradeCurrency.getCode(); - volumeColumnLabel.set(Res.get("offerbook.volume", code)); xAxis.setTickLabelFormatter(new StringConverter<>() { final int cryptoPrecision = 3; final DecimalFormat df = new DecimalFormat(",###"); @@ -230,15 +235,21 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(OfferDirection.BUY)); + sellButton.setId("sell-button-big"); buyHeaderLabel.setText(Res.get("market.offerBook.buyOffersHeaderLabel", viewBaseCurrencyCode)); - buyButton.updateText(Res.get("shared.buyCurrency", viewBaseCurrencyCode, viewPriceCurrencyCode)); + buyButton.updateText(Res.get( "shared.buyCurrency", viewBaseCurrencyCode)); + buyButton.setGraphic(GUIUtil.getCurrencyIconWithBorder(viewBaseCurrencyCode)); + buyButton.setOnAction(e -> model.goToOfferView(OfferDirection.SELL)); + buyButton.setId("buy-button-big"); priceColumnLabel.set(Res.get("shared.priceWithCur", viewPriceCurrencyCode)); @@ -288,8 +299,8 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(OfferDirection.BUY); sellTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.SELL); + buyTableRowSelectionListener = (observable, oldValue, newValue) -> model.goToOfferView(OfferDirection.BUY); havenoWindowVerticalSizeListener = (observable, oldValue, newValue) -> layout(); } @@ -345,12 +356,27 @@ public class OfferBookChartView extends ActivatableViewAndModel, VBox, Button, Label> getOfferTable(OfferDirection direction) { TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView, false); tableView.setMinHeight(initialOfferTableViewHeight); tableView.setPrefHeight(initialOfferTableViewHeight); tableView.setMinWidth(480); - tableView.getStyleClass().add("offer-table"); + tableView.getStyleClass().addAll("offer-table", "non-interactive-table"); // price TableColumn priceColumn = new TableColumn<>(); @@ -484,12 +511,14 @@ public class OfferBookChartView extends ActivatableViewAndModel volumeColumn = new TableColumn<>(); volumeColumn.setMinWidth(115); volumeColumn.setSortable(false); - volumeColumn.textProperty().bind(volumeColumnLabel); - volumeColumn.getStyleClass().addAll("number-column", "first-column"); + volumeColumn.textProperty().bind(isSellTable ? volumeSellColumnLabel : volumeBuyColumnLabel); + volumeColumn.getStyleClass().addAll("number-column"); volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( new Callback<>() { @@ -546,7 +575,8 @@ public class OfferBookChartView extends ActivatableViewAndModel amountColumn = new AutoTooltipTableColumn<>(Res.get("shared.XMRMinMax")); + TableColumn amountColumn = new TableColumn<>(); + amountColumn.textProperty().bind(isSellTable ? amountSellColumnLabel : amountBuyColumnLabel); amountColumn.setMinWidth(115); amountColumn.setSortable(false); amountColumn.getStyleClass().add("number-column"); @@ -570,10 +600,8 @@ public class OfferBookChartView extends ActivatableViewAndModel avatarColumn = new AutoTooltipTableColumn<>(isSellOffer ? + TableColumn avatarColumn = new AutoTooltipTableColumn<>(isSellTable ? Res.get("shared.sellerUpperCase") : Res.get("shared.buyerUpperCase")) { { setMinWidth(80); @@ -582,7 +610,7 @@ public class OfferBookChartView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); avatarColumn.setCellFactory( new Callback<>() { @@ -629,20 +657,16 @@ public class OfferBookChartView extends ActivatableViewAndModel model.goToOfferView(direction)); Region spacer = new Region(); @@ -653,9 +677,9 @@ public class OfferBookChartView extends ActivatableViewAndModel(tableView, vBox, button, titleLabel); diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java index c7d0c91277..0dbd449019 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -26,6 +26,7 @@ import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.TradeCurrency; import haveno.core.monetary.Price; +import haveno.core.monetary.Volume; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; @@ -58,6 +59,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.chart.XYChart; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -128,7 +130,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { list.addAll(c.getAddedSubList()); if (list.stream() .map(OfferBookListItem::getOffer) - .anyMatch(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()))) + .anyMatch(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()))) updateChartData(); } @@ -154,7 +156,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { synchronized (offerBookListItems) { List tradeCurrencyList = offerBookListItems.stream() .map(e -> { - String currencyCode = e.getOffer().getCurrencyCode(); + String currencyCode = e.getOffer().getCounterCurrencyCode(); Optional tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(currencyCode); return tradeCurrencyOptional.orElse(null); }) @@ -212,10 +214,42 @@ class OfferBookChartViewModel extends ActivatableViewModel { } public boolean isSellOffer(OfferDirection direction) { - // for cryptocurrency, buy direction is to buy XMR, so we need sell offers - // for traditional currency, buy direction is to sell XMR, so we need buy offers - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(getCurrencyCode()); - return isCryptoCurrency ? direction == OfferDirection.BUY : direction == OfferDirection.SELL; + return direction == OfferDirection.SELL; + } + + public double getTotalAmount(OfferDirection direction) { + synchronized (offerBookListItems) { + List offerList = offerBookListItems.stream() + .map(OfferBookListItem::getOffer) + .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + && e.getDirection().equals(direction)) + .collect(Collectors.toList()); + BigInteger sum = BigInteger.ZERO; + for (Offer offer : offerList) sum = sum.add(offer.getAmount()); + return HavenoUtils.atomicUnitsToXmr(sum); + } + } + + public Volume getTotalVolume(OfferDirection direction) { + synchronized (offerBookListItems) { + List volumes = offerBookListItems.stream() + .map(OfferBookListItem::getOffer) + .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + && e.getDirection().equals(direction)) + .map(Offer::getVolume) + .collect(Collectors.toList()); + try { + return VolumeUtil.sum(volumes); + } catch (Exception e) { + // log.error("Cannot compute total volume because prices are unavailable, currency={}, direction={}", + // selectedTradeCurrencyProperty.get().getCode(), direction); + return null; // expected before prices are available + } + } + } + + public boolean isCrypto() { + return CurrencyUtil.isCryptoCurrency(getCurrencyCode()); } public boolean isMyOffer(Offer offer) { @@ -266,13 +300,13 @@ class OfferBookChartViewModel extends ActivatableViewModel { } public int getMaxNumberOfPriceZeroDecimalsToColorize(Offer offer) { - return CurrencyUtil.isVolumeRoundedToNearestUnit(offer.getCurrencyCode()) + return CurrencyUtil.isVolumeRoundedToNearestUnit(offer.getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_UNIT : GUIUtil.NUM_DECIMALS_PRECISE; } public int getZeroDecimalsForPrice(Offer offer) { - return CurrencyUtil.isPricePrecise(offer.getCurrencyCode()) + return CurrencyUtil.isPricePrecise(offer.getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_PRECISE : GUIUtil.NUM_DECIMALS_PRICE_LESS_PRECISE; } @@ -316,13 +350,6 @@ class OfferBookChartViewModel extends ActivatableViewModel { // Offer price can be null (if price feed unavailable), thus a null-tolerant comparator is used. Comparator offerPriceComparator = Comparator.comparing(Offer::getPrice, Comparator.nullsLast(Comparator.naturalOrder())); - // Trading xmr-traditional is considered as buying/selling XMR, but trading xmr-crypto is - // considered as buying/selling Crypto. Because of this, when viewing a xmr-crypto pair, - // the buy column is actually the sell column and vice versa. To maintain the expected - // ordering, we have to reverse the price comparator. - boolean isCrypto = CurrencyUtil.isCryptoCurrency(getCurrencyCode()); -// if (isCrypto) offerPriceComparator = offerPriceComparator.reversed(); - // Offer amounts are used for the secondary sort. They are sorted from high to low. Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); @@ -333,11 +360,11 @@ class OfferBookChartViewModel extends ActivatableViewModel { offerPriceComparator .thenComparing(offerAmountComparator); - OfferDirection buyOfferDirection = isCrypto ? OfferDirection.SELL : OfferDirection.BUY; + OfferDirection buyOfferDirection = OfferDirection.BUY; List allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) - .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(buyOfferDirection)) .sorted(buyOfferSortComparator) .collect(Collectors.toList()); @@ -364,11 +391,11 @@ class OfferBookChartViewModel extends ActivatableViewModel { buildChartAndTableEntries(allBuyOffers, OfferDirection.BUY, buyData, topBuyOfferList); - OfferDirection sellOfferDirection = isCrypto ? OfferDirection.BUY : OfferDirection.SELL; + OfferDirection sellOfferDirection = OfferDirection.SELL; List allSellOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) - .filter(e -> e.getCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) + .filter(e -> e.getCounterCurrencyCode().equals(selectedTradeCurrencyProperty.get().getCode()) && e.getDirection().equals(sellOfferDirection)) .sorted(sellOfferSortComparator) .collect(Collectors.toList()); diff --git a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java index 9adf595ad4..150b7c888a 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadView.java @@ -65,6 +65,8 @@ public class SpreadView extends ActivatableViewAndModel(); + GUIUtil.applyTableStyle(tableView); + tableView.getStyleClass().add("non-interactive-table"); int gridRow = 0; GridPane.setRowIndex(tableView, gridRow); @@ -144,7 +146,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @@ -259,7 +261,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { @@ -289,7 +291,7 @@ public class SpreadView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewModel.java index 2ca1d3e743..9a2a63e51c 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/spread/SpreadViewModel.java @@ -116,11 +116,11 @@ class SpreadViewModel extends ActivatableViewModel { synchronized (offerBookListItems) { for (OfferBookListItem offerBookListItem : offerBookListItems) { Offer offer = offerBookListItem.getOffer(); - String key = offer.getCurrencyCode(); + String key = offer.getCounterCurrencyCode(); if (includePaymentMethod) { key = offer.getPaymentMethod().getShortName(); if (expandedView) { - key += ":" + offer.getCurrencyCode(); + key += ":" + offer.getCounterCurrencyCode(); } } if (!offersByCurrencyMap.containsKey(key)) @@ -134,8 +134,6 @@ class SpreadViewModel extends ActivatableViewModel { for (String key : offersByCurrencyMap.keySet()) { List offers = offersByCurrencyMap.get(key); - boolean iTraditionalCurrency = (offers.size() > 0 && offers.get(0).getPaymentMethod().isTraditional()); - List uniqueOffers = offers.stream().filter(distinctByKey(Offer::getId)).collect(Collectors.toList()); List buyOffers = uniqueOffers @@ -145,11 +143,7 @@ class SpreadViewModel extends ActivatableViewModel { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; if (a != b) { - if (iTraditionalCurrency) { - return a < b ? 1 : -1; - } else { - return a < b ? -1 : 1; - } + return a < b ? 1 : -1; } return 0; }) @@ -162,11 +156,7 @@ class SpreadViewModel extends ActivatableViewModel { long a = o1.getPrice() != null ? o1.getPrice().getValue() : 0; long b = o2.getPrice() != null ? o2.getPrice().getValue() : 0; if (a != b) { - if (iTraditionalCurrency) { - return a > b ? 1 : -1; - } else { - return a > b ? -1 : 1; - } + return a > b ? 1 : -1; } return 0; }) @@ -178,24 +168,22 @@ class SpreadViewModel extends ActivatableViewModel { Price bestSellOfferPrice = sellOffers.isEmpty() ? null : sellOffers.get(0).getPrice(); Price bestBuyOfferPrice = buyOffers.isEmpty() ? null : buyOffers.get(0).getPrice(); if (bestBuyOfferPrice != null && bestSellOfferPrice != null && - sellOffers.get(0).getCurrencyCode().equals(buyOffers.get(0).getCurrencyCode())) { - MarketPrice marketPrice = priceFeedService.getMarketPrice(sellOffers.get(0).getCurrencyCode()); + sellOffers.get(0).getCounterCurrencyCode().equals(buyOffers.get(0).getCounterCurrencyCode())) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(sellOffers.get(0).getCounterCurrencyCode()); // There have been some bug reports that an offer caused an overflow exception. // We never found out which offer it was. So add here a try/catch to get better info if it // happens again try { - if (iTraditionalCurrency) - spread = bestSellOfferPrice.subtract(bestBuyOfferPrice); - else - spread = bestBuyOfferPrice.subtract(bestSellOfferPrice); + spread = bestSellOfferPrice.subtract(bestBuyOfferPrice); // TODO maybe show extra columns with spread and use real amount diff // not % based. e.g. diff between best buy and sell offer (of small amounts its a smaller gain) if (spread != null && marketPrice != null && marketPrice.isPriceAvailable()) { double marketPriceAsDouble = marketPrice.getPrice(); - final double precision = iTraditionalCurrency ? + boolean isTraditionalCurrency = (offers.size() > 0 && offers.get(0).getPaymentMethod().isTraditional()); + final double precision = isTraditionalCurrency ? Math.pow(10, TraditionalMoney.SMALLEST_UNIT_EXPONENT) : Math.pow(10, CryptoMoney.SMALLEST_UNIT_EXPONENT); @@ -217,8 +205,8 @@ class SpreadViewModel extends ActivatableViewModel { "Details of offer data: \n" + "bestSellOfferPrice: " + bestSellOfferPrice.getValue() + "\n" + "bestBuyOfferPrice: " + bestBuyOfferPrice.getValue() + "\n" + - "sellOffer getCurrencyCode: " + sellOffers.get(0).getCurrencyCode() + "\n" + - "buyOffer getCurrencyCode: " + buyOffers.get(0).getCurrencyCode() + "\n\n" + + "sellOffer getCurrencyCode: " + sellOffers.get(0).getCounterCurrencyCode() + "\n" + + "buyOffer getCurrencyCode: " + buyOffers.get(0).getCounterCurrencyCode() + "\n\n" + "Please copy and paste this data and send it to the developers so they can investigate the issue."; new Popup().error(msg).show(); log.error(t.toString()); diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java index c8ba1af3af..b3d8687849 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/ChartCalculations.java @@ -22,13 +22,16 @@ import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.TraditionalMoney; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.desktop.main.market.trades.charts.CandleData; import haveno.desktop.util.DisplayUtils; import javafx.scene.chart.XYChart; import javafx.util.Pair; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import java.math.BigInteger; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @@ -47,6 +50,7 @@ import java.util.stream.Collectors; import static haveno.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; +@Slf4j public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); @@ -76,7 +80,7 @@ public class ChartCalculations { dateMapsPerTickUnit.forEach((tick, map) -> { HashMap priceMap = new HashMap<>(); - map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAverageTraditionalPrice(tradeStatisticsList))); usdAveragePriceMapsPerTickUnit.put(tick, priceMap); }); return usdAveragePriceMapsPerTickUnit; @@ -210,16 +214,16 @@ public class ChartCalculations { return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); } - private static long getAveragePrice(List tradeStatisticsList) { - long accumulatedAmount = 0; // TODO: use BigInteger - long accumulatedVolume = 0; + private static long getAverageTraditionalPrice(List tradeStatisticsList) { + BigInteger accumulatedAmount = BigInteger.ZERO; + BigInteger accumulatedVolume = BigInteger.ZERO; for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { - accumulatedAmount += tradeStatistics.getAmount(); - accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + accumulatedAmount = accumulatedAmount.add(BigInteger.valueOf(tradeStatistics.getAmount())); + accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(tradeStatistics.getTradeVolume().getValue())); } - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, 4 + TraditionalMoney.SMALLEST_UNIT_EXPONENT); - return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); + BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, TraditionalMoney.SMALLEST_UNIT_EXPONENT + 4); + return MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); } @VisibleForTesting @@ -232,8 +236,8 @@ public class ChartCalculations { long close = 0; long high = 0; long low = 0; - long accumulatedVolume = 0; // TODO: use BigInteger - long accumulatedAmount = 0; + BigInteger accumulatedVolume = BigInteger.ZERO; + BigInteger accumulatedAmount = BigInteger.ZERO; long numTrades = set.size(); List tradePrices = new ArrayList<>(); for (TradeStatistics3 item : set) { @@ -242,8 +246,8 @@ public class ChartCalculations { low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; - accumulatedVolume += item.getTradeVolume().getValue(); - accumulatedAmount += item.getTradeAmount().longValueExact(); + accumulatedVolume = accumulatedVolume.add(BigInteger.valueOf(item.getTradeVolume().getValue())); + accumulatedAmount = accumulatedAmount.add(item.getTradeAmount()); tradePrices.add(tradePriceAsLong); } Collections.sort(tradePrices); @@ -259,16 +263,10 @@ public class ChartCalculations { Long[] prices = new Long[tradePrices.size()]; tradePrices.toArray(prices); long medianPrice = MathUtils.getMedian(prices); - boolean isBullish; - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - isBullish = close < open; - double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, 4 + CryptoMoney.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / accumulatedVolume); - } else { - isBullish = close > open; - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, 4 + TraditionalMoney.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / accumulatedAmount); - } + boolean isBullish = close > open; + int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT; + BigInteger accumulatedVolumeAsBI = MathUtils.scaleUpByPowerOf10(accumulatedVolume, smallestUnitExponent + 4); + averagePrice = MathUtils.roundDoubleToLong(HavenoUtils.divide(accumulatedVolumeAsBI, accumulatedAmount)); Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval)); @@ -277,11 +275,11 @@ public class ChartCalculations { DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. - averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, TraditionalMoney.SMALLEST_UNIT_EXPONENT); - long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, smallestUnitExponent); + long volumeInUsd = averageUsdPrice * MathUtils.scaleDownByPowerOf10(accumulatedAmount, 4).longValue(); // We store USD value without decimals as its only total volume, no precision is needed. - volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, TraditionalMoney.SMALLEST_UNIT_EXPONENT); - return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, + volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, smallestUnitExponent); + return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount.longValueExact(), accumulatedVolume.longValueExact(), numTrades, isBullish, dateString, volumeInUsd); } diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index 97fcce06d1..d087329942 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -116,15 +116,11 @@ public class TradesChartsView extends ActivatableViewAndModel currencyItem.codeDashNameString().equals(query)). findAny().orElse(null); } - - private CurrencyListItem specialShowAllItem() { - return comboBox.getItems().get(0); - } } private final User user; @@ -291,7 +287,7 @@ public class TradesChartsView extends ActivatableViewAndModel UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) - currencyComboBox.getSelectionModel().select(SHOW_ALL); + return; CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); @@ -397,7 +393,7 @@ public class TradesChartsView extends ActivatableViewAndModel> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size() + 1; boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get(); @@ -686,6 +682,7 @@ public class TradesChartsView extends ActivatableViewAndModel(); + GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.ALWAYS); + tableView.getStyleClass().add("non-interactive-table"); // date TableColumn dateColumn = new AutoTooltipTableColumn<>(Res.get("shared.dateTime")) { @@ -739,7 +738,7 @@ public class TradesChartsView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); dateColumn.setCellFactory( new Callback<>() { @@ -865,7 +864,7 @@ public class TradesChartsView extends ActivatableViewAndModel paymentMethodColumn = new AutoTooltipTableColumn<>(Res.get("shared.paymentMethod")); - paymentMethodColumn.getStyleClass().add("number-column"); + paymentMethodColumn.getStyleClass().addAll("number-column"); paymentMethodColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); paymentMethodColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsViewModel.java index 665f25aa3e..022c3cbdc6 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsViewModel.java @@ -265,7 +265,7 @@ class TradesChartsViewModel extends ActivatableViewModel { return; } if (throwable != null) { - log.error("Error at applyAsyncChartData. {}", throwable.toString()); + log.error("Error at applyAsyncChartData. {}", throwable); return; } UserThread.execute(() -> { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index 47bfee0006..bcb18c32c9 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -168,7 +168,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { reserveExactAmount = preferences.getSplitOfferOutput(); useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice()); - securityDepositPct.set(Restrictions.getMinSecurityDepositAsPercent()); + securityDepositPct.set(Restrictions.getMinSecurityDepositPct()); paymentAccountsChangeListener = change -> fillPaymentAccounts(); } @@ -333,12 +333,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { setSuggestedSecurityDeposit(getPaymentAccount()); if (amount.get() != null && this.allowAmountUpdate) - this.amount.set(amount.get().min(BigInteger.valueOf(getMaxTradeLimit()))); + this.amount.set(amount.get().min(getMaxTradeLimit())); } } private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { - var minSecurityDeposit = Restrictions.getMinSecurityDepositAsPercent(); + var minSecurityDeposit = Restrictions.getMinSecurityDepositPct(); try { if (getTradeCurrency() == null) { setSecurityDepositPct(minSecurityDeposit); @@ -369,7 +369,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } // Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit var suggestedSecurityDeposit = - Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositAsPercent()); + Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositPct()); securityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); } catch (Throwable t) { log.error(t.toString()); @@ -472,18 +472,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return marketPriceMarginPct; } - long getMaxTradeLimit() { + BigInteger getMinTradeLimit() { + return Restrictions.getMinTradeAmount(); + } - // disallow offers which no buyer can take due to trade limits on release - if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get()); - } - - if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); - } else { - return 0; - } + BigInteger getMaxTradeLimit() { + return offerUtil.getMaxTradeLimitForRelease(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -543,9 +537,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel { // if the volume != amount * price, we need to adjust the amount if (amount.get() == null || !volumeBefore.equals(price.get().getVolumeByAmount(amount.get()))) { BigInteger value = price.get().getAmountByVolume(volumeBefore); - value = value.min(BigInteger.valueOf(getMaxTradeLimit())); // adjust if above maximum - value = value.max(Restrictions.getMinTradeAmount()); // adjust if below minimum - value = CoinUtil.getRoundedAmount(value, price.get(), getMaxTradeLimit(), tradeCurrencyCode.get(), paymentAccount.getPaymentMethod().getId()); + BigInteger minAmount = getMinTradeLimit(); + BigInteger maxAmount = getMaxTradeLimit(); + value = value.max(minAmount); // adjust if below minimum + value = value.min(maxAmount); // adjust if above maximum + value = CoinUtil.getRoundedAmount(value, price.get(), minAmount, maxAmount, tradeCurrencyCode.get(), paymentAccount.getPaymentMethod().getId()); amount.set(value); } @@ -693,7 +689,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, offer.getAmount()); return Math.min(offerSellerSecurityDepositAsPercent, - Restrictions.getMaxSecurityDepositAsPercent()); + Restrictions.getMaxSecurityDepositPct()); } ReadOnlyObjectProperty totalToPayAsProperty() { @@ -701,7 +697,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } public BigInteger getMaxMakerFee() { - return HavenoUtils.multiply(amount.get(), buyerAsTakerWithoutDeposit.get() ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); + return HavenoUtils.multiply(amount.get(), HavenoUtils.getMakerFeePct(tradeCurrencyCode.get(), buyerAsTakerWithoutDeposit.get())); } boolean canPlaceOffer() { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 31c02bdc0d..24af6600bf 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -85,6 +85,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.util.StringConverter; @@ -149,7 +150,8 @@ public abstract class MutableOfferView> exten private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; private ImageView qrCodeImageView; - private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; + private StackPane qrCodePane; + private VBox paymentAccountsSelection, currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; @@ -264,6 +266,11 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.setText(model.triggerPrice.get()); extraInfoTextArea.setText(model.dataModel.extraInfo.get()); + + // show or hide elements based on current screen + if (model.showPayFundsScreenDisplayed.get()) { + onShowPayFundsScreen(); + } } } @@ -287,7 +294,7 @@ public abstract class MutableOfferView> exten @Override public void onTabSelected(boolean isSelected) { - if (isSelected && !model.getDataModel().isTabSelected) { + if (isSelected) { doActivate(); } else { deactivate(); @@ -308,7 +315,7 @@ public abstract class MutableOfferView> exten if (!result) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -319,20 +326,12 @@ public abstract class MutableOfferView> exten if (OfferViewUtil.isShownAsBuyOffer(direction, tradeCurrency)) { placeOfferButton.setId("buy-button-big"); - if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.buy")); - } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), tradeCurrency.getCode()); - } + placeOfferButtonLabel = Res.get("createOffer.placeOfferButton.buy", tradeCurrency.getCode()); nextButton.setId("buy-button"); fundFromSavingsWalletButton.setId("buy-button"); } else { placeOfferButton.setId("sell-button-big"); - if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.sell")); - } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), tradeCurrency.getCode()); - } + placeOfferButtonLabel = Res.get("createOffer.placeOfferButton.sell", tradeCurrency.getCode()); nextButton.setId("sell-button"); fundFromSavingsWalletButton.setId("sell-button"); } @@ -380,8 +379,6 @@ public abstract class MutableOfferView> exten } private void onShowPayFundsScreen() { - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - nextButton.setVisible(false); nextButton.setManaged(false); nextButton.setOnAction(null); @@ -427,7 +424,8 @@ public abstract class MutableOfferView> exten totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); }); - paymentAccountsComboBox.setDisable(true); + paymentAccountsSelection.setDisable(true); + currencySelection.setDisable(true); editOfferElements.forEach(node -> { node.setMouseTransparent(true); @@ -445,20 +443,14 @@ public abstract class MutableOfferView> exten // temporarily disabled due to high CPU usage (per issue #4649) // waitingForFundsSpinner.play(); - payFundsTitledGroupBg.setVisible(true); - totalToPayTextField.setVisible(true); - addressTextField.setVisible(true); - qrCodeImageView.setVisible(true); - balanceTextField.setVisible(true); - cancelButton2.setVisible(true); - reserveExactAmountSlider.setVisible(true); + showFundingGroup(); } private void updateOfferElementsStyle() { GridPane.setColumnSpan(firstRowHBox, 2); - String activeInputStyle = "input-with-border"; - String readOnlyInputStyle = "input-with-border-readonly"; + String activeInputStyle = "offer-input"; + String readOnlyInputStyle = "offer-input-readonly"; amountValueCurrencyBox.getStyleClass().remove(activeInputStyle); amountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().remove(activeInputStyle); @@ -546,7 +538,15 @@ public abstract class MutableOfferView> exten } private void onCurrencyComboBoxSelected() { - model.onCurrencySelected(currencyComboBox.getSelectionModel().getSelectedItem()); + TradeCurrency currency = currencyComboBox.getSelectionModel().getSelectedItem(); + model.onCurrencySelected(currency); + + // update the place offer button text + if (OfferViewUtil.isShownAsBuyOffer(model.getDataModel().getDirection(), currency)) { + placeOfferButton.updateText( Res.get("createOffer.placeOfferButton.buy", currency.getCode())); + } else { + placeOfferButton.updateText(Res.get("createOffer.placeOfferButton.sell", currency.getCode())); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -610,6 +610,7 @@ public abstract class MutableOfferView> exten paymentTitledGroupBg.managedProperty().bind(paymentTitledGroupBg.visibleProperty()); currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty()); + currencyTextFieldBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyTextFieldBox.managedProperty().bind(currencyTextFieldBox.visibleProperty()); } @@ -717,7 +718,11 @@ public abstract class MutableOfferView> exten }; extraInfoFocusedListener = (observable, oldValue, newValue) -> { model.onFocusOutExtraInfoTextArea(oldValue, newValue); - extraInfoTextArea.setText(model.extraInfo.get()); + + // avoid setting text area to empty text because blinking caret does not appear + if (model.extraInfo.get() != null && !model.extraInfo.get().isEmpty()) { + extraInfoTextArea.setText(model.extraInfo.get()); + } }; errorMessageListener = (o, oldValue, newValue) -> { @@ -736,15 +741,6 @@ public abstract class MutableOfferView> exten marketBasedPriceTextField.clear(); volumeTextField.clear(); triggerPriceInputTextField.clear(); - if (!CurrencyUtil.isTraditionalCurrency(newValue)) { - if (model.isShownAsBuyOffer()) { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), - model.getTradeCurrency().getCode())); - } else { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), - model.getTradeCurrency().getCode())); - } - } }; placeOfferCompletedListener = (o, oldValue, newValue) -> { @@ -757,7 +753,7 @@ public abstract class MutableOfferView> exten UserThread.runAfter(() -> new Popup().headLine(Res.get("createOffer.success.headline")) .feedback(Res.get("createOffer.success.info")) .dontShowAgainId(key) - .actionButtonTextWithGoTo("navigation.portfolio.myOpenOffers") + .actionButtonTextWithGoTo("portfolio.tab.openOffers") .onAction(this::closeAndGoToOpenOffers) .onClose(this::closeAndGoToOpenOffers) .show(), @@ -981,11 +977,12 @@ public abstract class MutableOfferView> exten private void addGridPane() { gridPane = new GridPane(); gridPane.getStyleClass().add("content-pane"); - gridPane.setPadding(new Insets(25, 25, -1, 25)); + gridPane.setPadding(new Insets(25, 25, 25, 25)); gridPane.setHgap(5); gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); } private void addPaymentGroup() { @@ -1002,8 +999,9 @@ public abstract class MutableOfferView> exten final Tuple3> currencyBoxTuple = addTopLabelComboBox( Res.get("shared.currency"), Res.get("list.currency.select")); + paymentAccountsSelection = tradingAccountBoxTuple.first; currencySelection = currencyBoxTuple.first; - paymentGroupBox.getChildren().addAll(tradingAccountBoxTuple.first, currencySelection); + paymentGroupBox.getChildren().addAll(paymentAccountsSelection, currencySelection); GridPane.setRowIndex(paymentGroupBox, gridRow); GridPane.setColumnSpan(paymentGroupBox, 2); @@ -1014,11 +1012,13 @@ public abstract class MutableOfferView> exten paymentAccountsComboBox = tradingAccountBoxTuple.third; paymentAccountsComboBox.setMinWidth(tradingAccountBoxTuple.first.getMinWidth()); paymentAccountsComboBox.setPrefWidth(tradingAccountBoxTuple.first.getMinWidth()); - editOfferElements.add(tradingAccountBoxTuple.first); + paymentAccountsComboBox.getStyleClass().add("input-with-border"); + editOfferElements.add(paymentAccountsSelection); // we display either currencyComboBox (multi currency account) or currencyTextField (single) currencyComboBox = currencyBoxTuple.third; currencyComboBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); + currencyComboBox.getStyleClass().add("input-with-border"); editOfferElements.add(currencySelection); currencyComboBox.setConverter(new StringConverter<>() { @Override @@ -1035,6 +1035,7 @@ public abstract class MutableOfferView> exten final Tuple3 currencyTextFieldTuple = addTopLabelTextField(gridPane, gridRow, Res.get("shared.currency"), "", 5d); currencyTextField = currencyTextFieldTuple.second; currencyTextFieldBox = currencyTextFieldTuple.third; + currencyTextFieldBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); currencyTextFieldBox.setVisible(false); editOfferElements.add(currencyTextFieldBox); @@ -1101,6 +1102,7 @@ public abstract class MutableOfferView> exten GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); extraInfoTextArea = new InputTextArea(); + extraInfoTextArea.setText(""); extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); extraInfoTextArea.getStyleClass().add("text-area"); extraInfoTextArea.setWrapText(true); @@ -1179,6 +1181,40 @@ public abstract class MutableOfferView> exten cancelButton1.setManaged(false); } + protected void hideFundingGroup() { + payFundsTitledGroupBg.setVisible(false); + payFundsTitledGroupBg.setManaged(false); + totalToPayTextField.setVisible(false); + totalToPayTextField.setManaged(false); + addressTextField.setVisible(false); + addressTextField.setManaged(false); + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); + balanceTextField.setVisible(false); + balanceTextField.setManaged(false); + cancelButton2.setVisible(false); + cancelButton2.setManaged(false); + reserveExactAmountSlider.setVisible(false); + reserveExactAmountSlider.setManaged(false); + } + + protected void showFundingGroup() { + payFundsTitledGroupBg.setVisible(true); + payFundsTitledGroupBg.setManaged(true); + totalToPayTextField.setVisible(true); + totalToPayTextField.setManaged(true); + addressTextField.setVisible(true); + addressTextField.setManaged(true); + qrCodePane.setVisible(true); + qrCodePane.setManaged(true); + balanceTextField.setVisible(true); + balanceTextField.setManaged(true); + cancelButton2.setVisible(true); + cancelButton2.setManaged(true); + reserveExactAmountSlider.setVisible(true); + reserveExactAmountSlider.setManaged(true); + } + private VBox getSecurityDepositBox() { Tuple3 tuple = getEditableValueBoxWithInfo( Res.get("createOffer.securityDeposit.prompt")); @@ -1212,21 +1248,23 @@ public abstract class MutableOfferView> exten totalToPayTextField.setVisible(false); GridPane.setMargin(totalToPayTextField, new Insets(60 + heightAdjustment, 10, 0, 0)); - qrCodeImageView = new ImageView(); - qrCodeImageView.setVisible(false); - qrCodeImageView.setFitHeight(150); - qrCodeImageView.setFitWidth(150); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setRowSpan(qrCodeImageView, 3); - GridPane.setValignment(qrCodeImageView, VPos.BOTTOM); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setRowSpan(qrCodePane, 3); + GridPane.setValignment(qrCodePane, VPos.BOTTOM); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); + + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); @@ -1326,6 +1364,8 @@ public abstract class MutableOfferView> exten }); cancelButton2.setDefaultButton(false); cancelButton2.setVisible(false); + + hideFundingGroup(); } private void openWallet() { @@ -1479,7 +1519,7 @@ public abstract class MutableOfferView> exten // Fixed/Percentage toggle priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); editOfferElements.add(priceTypeToggleButton); - HBox.setMargin(priceTypeToggleButton, new Insets(16, 1.5, 0, 0)); + HBox.setMargin(priceTypeToggleButton, new Insets(25, 1.5, 0, 0)); priceTypeToggleButton.setOnAction((actionEvent) -> updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue())); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fb971b6296..96c083cb73 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -19,6 +19,8 @@ package haveno.desktop.main.offer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.handlers.ErrorMessageHandler; @@ -108,7 +110,7 @@ public abstract class MutableOfferViewModel ext private String amountDescription; private String addressAsString; private final String paymentLabel; - private boolean createOfferRequested; + private boolean createOfferInProgress; public boolean createOfferCanceled; public final StringProperty amount = new SimpleStringProperty(); @@ -205,7 +207,6 @@ public abstract class MutableOfferViewModel ext @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); - this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; @@ -260,15 +261,11 @@ public abstract class MutableOfferViewModel ext private void addBindings() { if (dataModel.getDirection() == OfferDirection.BUY) { volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get(CurrencyUtil.isTraditionalCurrency(dataModel.getTradeCurrencyCode().get()) ? - "createOffer.amountPriceBox.buy.volumeDescription" : - "createOffer.amountPriceBox.buy.volumeDescriptionCrypto", dataModel.getTradeCurrencyCode().get()), + () -> Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } else { volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get(CurrencyUtil.isTraditionalCurrency(dataModel.getTradeCurrencyCode().get()) ? - "createOffer.amountPriceBox.sell.volumeDescription" : - "createOffer.amountPriceBox.sell.volumeDescriptionCrypto", dataModel.getTradeCurrencyCode().get()), + () -> Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getTradeCurrencyCode().get()), dataModel.getTradeCurrencyCode())); } volumePromptLabel.bind(createStringBinding( @@ -318,7 +315,6 @@ public abstract class MutableOfferViewModel ext }; priceStringListener = (ov, oldValue, newValue) -> { updateMarketPriceAvailable(); - final String currencyCode = dataModel.getTradeCurrencyCode().get(); if (!ignorePriceStringListener) { if (isPriceInputValid(newValue).isValid) { setPriceToModel(); @@ -331,9 +327,7 @@ public abstract class MutableOfferViewModel ext try { double priceAsDouble = ParsingUtils.parseNumberStringToDouble(price.get()); double relation = priceAsDouble / marketPriceAsDouble; - final OfferDirection compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? - OfferDirection.SELL : - OfferDirection.BUY; + final OfferDirection compareDirection = OfferDirection.BUY; double percentage = dataModel.getDirection() == compareDirection ? 1 - relation : relation - 1; percentage = MathUtils.roundDouble(percentage, 4); dataModel.setMarketPriceMarginPct(percentage); @@ -366,9 +360,7 @@ public abstract class MutableOfferViewModel ext if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { percentage = MathUtils.roundDouble(percentage, 4); double marketPriceAsDouble = marketPrice.getPrice(); - final OfferDirection compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? - OfferDirection.SELL : - OfferDirection.BUY; + final OfferDirection compareDirection = OfferDirection.BUY; double factor = dataModel.getDirection() == compareDirection ? 1 - percentage : 1 + percentage; @@ -490,7 +482,8 @@ public abstract class MutableOfferViewModel ext buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); + xmrValidator.setMinValue(dataModel.getMinTradeLimit()); if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); updateSecurityDeposit(); setSecurityDepositToModel(); @@ -502,7 +495,7 @@ public abstract class MutableOfferViewModel ext extraInfoStringListener = (ov, oldValue, newValue) -> { if (newValue != null) { - extraInfo.set(newValue); + extraInfo.set(newValue.trim()); UserThread.execute(() -> onExtraInfoTextAreaChanged()); } }; @@ -608,20 +601,13 @@ public abstract class MutableOfferViewModel ext } if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); - xmrValidator.setMinValue(Restrictions.getMinTradeAmount()); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); + xmrValidator.setMinValue(dataModel.getMinTradeLimit()); final boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; - boolean isTraditionalCurrency = CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode()); - - if (isTraditionalCurrency) { - amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", - isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); - } else { - amountDescription = Res.get(isBuy ? "createOffer.amountPriceBox.sell.amountDescriptionCrypto" : - "createOffer.amountPriceBox.buy.amountDescriptionCrypto"); - } + amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", + isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); validateAndSetSecurityDepositToModel(); @@ -638,32 +624,37 @@ public abstract class MutableOfferViewModel ext /////////////////////////////////////////////////////////////////////////////////////////// void onPlaceOffer(Offer offer, Runnable resultHandler) { - errorMessage.set(null); - createOfferRequested = true; - createOfferCanceled = false; - - dataModel.onPlaceOffer(offer, transaction -> { - resultHandler.run(); - if (!createOfferCanceled) placeOfferCompleted.set(true); + ThreadUtils.execute(() -> { errorMessage.set(null); - }, errMessage -> { - createOfferRequested = false; - if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); - else errorMessage.set(errMessage); + createOfferInProgress = true; + createOfferCanceled = false; + + dataModel.onPlaceOffer(offer, transaction -> { + createOfferInProgress = false; + resultHandler.run(); + if (!createOfferCanceled) placeOfferCompleted.set(true); + errorMessage.set(null); + }, errMessage -> { + createOfferInProgress = false; + if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); + else errorMessage.set(errMessage); + + UserThread.execute(() -> { + updateButtonDisableState(); + updateSpinnerInfo(); + resultHandler.run(); + }); + }); UserThread.execute(() -> { updateButtonDisableState(); updateSpinnerInfo(); - resultHandler.run(); }); - }); - - updateButtonDisableState(); - updateSpinnerInfo(); + }, getClass().getSimpleName()); } public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - createOfferRequested = false; + log.info("Canceling posting offer {}", offer.getId()); createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); @@ -693,7 +684,7 @@ public abstract class MutableOfferViewModel ext amountValidationResult.set(isXmrInputValid(amount.get())); xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); securityDepositValidator.setPaymentAccount(paymentAccount); } @@ -720,7 +711,7 @@ public abstract class MutableOfferViewModel ext new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true), HavenoUtils.formatXmr(dataModel.getTotalBalance(), true))) - .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); } @@ -751,7 +742,7 @@ public abstract class MutableOfferViewModel ext if (minAmount.get() != null) minAmountValidationResult.set(isXmrInputValid(minAmount.get())); } else if (amount.get() != null && xmrValidator.getMaxTradeLimit() != null && xmrValidator.getMaxTradeLimit().longValueExact() == OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.longValueExact()) { - if (ParsingUtils.parseNumberStringToDouble(amount.get()) < HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount())) { + if (ParsingUtils.parseNumberStringToDouble(amount.get()) < HavenoUtils.atomicUnitsToXmr(dataModel.getMinTradeLimit())) { amountValidationResult.set(result); } else { amount.set(HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit())); @@ -990,7 +981,7 @@ public abstract class MutableOfferViewModel ext InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); securityDepositValidationResult.set(result); if (result.isValid) { - double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositAsPercent(); + double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositPct(); String key = "buyerSecurityDepositIsLowerAsDefault"; double depositAsDouble = ParsingUtils.parsePercentStringToDouble(securityDeposit.get()); if (preferences.showAgain(key) && depositAsDouble < defaultSecurityDeposit) { @@ -1049,7 +1040,7 @@ public abstract class MutableOfferViewModel ext FormattingUtils.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) .actionButtonText(Res.get("createOffer.changePrice")) .onAction(popup::hide) - .closeButtonTextWithGoTo("navigation.settings.preferences") + .closeButtonTextWithGoTo("settings.tab.preferences") .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) .show(); } @@ -1158,29 +1149,14 @@ public abstract class MutableOfferViewModel ext } String getTriggerPriceDescriptionLabel() { - String details; - if (dataModel.isBuyOffer()) { - details = dataModel.isCryptoCurrency() ? - Res.get("account.notifications.marketAlert.message.msg.below") : - Res.get("account.notifications.marketAlert.message.msg.above"); - } else { - details = dataModel.isCryptoCurrency() ? - Res.get("account.notifications.marketAlert.message.msg.above") : - Res.get("account.notifications.marketAlert.message.msg.below"); - } + String details = dataModel.isBuyOffer() ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); return Res.get("createOffer.triggerPrice.label", details); } String getPercentagePriceDescription() { - if (dataModel.isBuyOffer()) { - return dataModel.isCryptoCurrency() ? - Res.get("shared.aboveInPercent") : - Res.get("shared.belowInPercent"); - } else { - return dataModel.isCryptoCurrency() ? - Res.get("shared.belowInPercent") : - Res.get("shared.aboveInPercent"); - } + return dataModel.isBuyOffer() ? Res.get("shared.belowInPercent") : Res.get("shared.aboveInPercent"); } @@ -1192,10 +1168,9 @@ public abstract class MutableOfferViewModel ext if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); - long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); if (price != null && price.isPositive()) { - amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); + amount = CoinUtil.getRoundedAmount(amount, price, dataModel.getMinTradeLimit(), dataModel.getMaxTradeLimit(), tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setAmount(amount); if (syncMinAmountWithAmount || @@ -1214,9 +1189,8 @@ public abstract class MutableOfferViewModel ext BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); - long maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null && price.isPositive()) { - minAmount = CoinUtil.getRoundedAmount(minAmount, price, maxTradeLimit, tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); + minAmount = CoinUtil.getRoundedAmount(minAmount, price, dataModel.getMinTradeLimit(), dataModel.getMaxTradeLimit(), tradeCurrencyCode.get(), dataModel.getPaymentAccount().getPaymentMethod().getId()); } dataModel.setMinAmount(minAmount); @@ -1253,7 +1227,7 @@ public abstract class MutableOfferViewModel ext if (securityDeposit.get() != null && !securityDeposit.get().isEmpty() && !isMinSecurityDeposit.get()) { dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { - dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositPct()); } } @@ -1269,7 +1243,7 @@ public abstract class MutableOfferViewModel ext // If the security deposit in the model is not valid percent String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); if (!securityDepositValidator.validate(value).isValid) { - dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositPct()); } } @@ -1325,7 +1299,7 @@ public abstract class MutableOfferViewModel ext } else { boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? - Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer + Restrictions.getDefaultSecurityDepositPct() : // use default percent if no deposit from buyer dataModel.getSecurityDepositPct().get())); } } @@ -1355,12 +1329,12 @@ public abstract class MutableOfferViewModel ext inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; isNextButtonDisabled.set(!inputDataValid); - isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); + isPlaceOfferButtonDisabled.set(createOfferInProgress || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } private ValidationResult getExtraInfoValidationResult() { - if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { - return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.MAX_EXTRA_INFO_LENGTH)); + if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.getMaxExtraInfoLength()) { + return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.getMaxExtraInfoLength())); } else { return new InputValidator.ValidationResult(true); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java index 6f6aba7cbc..3d26a6ff38 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java @@ -138,7 +138,7 @@ public abstract class OfferView extends ActivatableView { @Override public void onTakeOffer(Offer offer) { - Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(offer.getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && canCreateOrTakeOffer(optionalTradeCurrency.get())) { showTakeOffer(offer); } @@ -192,12 +192,10 @@ public abstract class OfferView extends ActivatableView { loadTakeViewClass(viewClass, childViewClass, cryptoOfferBookTab); } else { // add sanity check in case of app restart - if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { - Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? - CurrencyUtil.getTradeCurrency(preferences.getSellScreenCryptoCurrencyCode()) : - CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCryptoCurrencyCode()); - tradeCurrency = tradeCurrencyOptional.isEmpty() ? OfferViewUtil.getAnyOfMainCryptoCurrencies() : tradeCurrencyOptional.get(); - } + Optional tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ? + CurrencyUtil.getTradeCurrency(preferences.getSellScreenCryptoCurrencyCode()) : + CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCryptoCurrencyCode()); + tradeCurrency = tradeCurrencyOptional.isEmpty() ? OfferViewUtil.getAnyOfMainCryptoCurrencies() : tradeCurrencyOptional.get(); loadCreateViewClass(cryptoOfferBookView, viewClass, childViewClass, cryptoOfferBookTab, (PaymentMethod) data); } tabPane.getSelectionModel().select(cryptoOfferBookTab); @@ -220,14 +218,14 @@ public abstract class OfferView extends ActivatableView { labelTab.setClosable(false); Label offerLabel = new Label(getOfferLabel()); // use overlay for label for custom formatting offerLabel.getStyleClass().add("titled-group-bg-label"); - offerLabel.setStyle("-fx-font-size: 1.4em;"); + offerLabel.setStyle("-fx-font-size: 1.3em;"); labelTab.setGraphic(offerLabel); - fiatOfferBookTab = new Tab(Res.get("shared.fiat").toUpperCase()); + fiatOfferBookTab = new Tab(Res.get("shared.fiat")); fiatOfferBookTab.setClosable(false); - cryptoOfferBookTab = new Tab(Res.get("shared.crypto").toUpperCase()); + cryptoOfferBookTab = new Tab(Res.get("shared.crypto")); cryptoOfferBookTab.setClosable(false); - otherOfferBookTab = new Tab(Res.get("shared.other").toUpperCase()); + otherOfferBookTab = new Tab(Res.get("shared.other")); otherOfferBookTab.setClosable(false); tabPane.getTabs().addAll(labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab); } @@ -316,7 +314,7 @@ public abstract class OfferView extends ActivatableView { private void showTakeOffer(Offer offer) { this.offer = offer; - Class> offerBookViewClass = getOfferBookViewClassFor(offer.getCurrencyCode()); + Class> offerBookViewClass = getOfferBookViewClassFor(offer.getCounterCurrencyCode()); navigation.navigateTo(MainView.class, this.getClass(), offerBookViewClass, TakeOfferView.class); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java index a01f2f3e96..9f89e69662 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java @@ -100,7 +100,7 @@ public class OfferViewUtil { } public static boolean isShownAsSellOffer(Offer offer) { - return isShownAsSellOffer(offer.getCurrencyCode(), offer.getDirection()); + return isShownAsSellOffer(offer.getCounterCurrencyCode(), offer.getDirection()); } public static boolean isShownAsSellOffer(TradeCurrency tradeCurrency, OfferDirection direction) { @@ -140,7 +140,7 @@ public class OfferViewUtil { public static void submitTransactionHex(XmrWalletService xmrWalletService, TableView tableView, String reserveTxHex) { - MoneroSubmitTxResult result = xmrWalletService.getDaemon().submitTxHex(reserveTxHex); + MoneroSubmitTxResult result = xmrWalletService.getMonerod().submitTxHex(reserveTxHex); log.info("submitTransactionHex: reserveTxHex={} result={}", result); tableView.refresh(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java index e97c08558e..945765bcb9 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java @@ -106,9 +106,9 @@ public class CryptoOfferBookViewModel extends OfferBookViewModel { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; // offer to buy xmr appears as offer to sell in peer's offer book and vice versa - boolean currencyResult = CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode()) && + boolean currencyResult = CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode()) && (showAllTradeCurrenciesProperty.get() || - offer.getCurrencyCode().equals(selectedTradeCurrency.getCode())); + offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode())); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java index bea41743b6..aef2c57c6f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java @@ -116,7 +116,7 @@ public class FiatOfferBookViewModel extends OfferBookViewModel { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; boolean currencyResult = (showAllTradeCurrenciesProperty.get() && offer.isFiatOffer()) || - offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()); + offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode()); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java index 2e92a9d42d..6f5424a843 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBook.java @@ -234,7 +234,7 @@ public class OfferBook { final String[] ccyCode = new String[1]; final int[] offerCount = new int[1]; offerBookListItems.forEach(o -> { - ccyCode[0] = o.getOffer().getCurrencyCode(); + ccyCode[0] = o.getOffer().getCounterCurrencyCode(); if (o.getOffer().getDirection() == BUY) { offerCount[0] = buyOfferCountMap.getOrDefault(ccyCode[0], 0) + 1; buyOfferCountMap.put(ccyCode[0], offerCount[0]); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookListItem.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookListItem.java index e340f9676c..353d4e87d4 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookListItem.java @@ -67,9 +67,9 @@ public class OfferBookListItem { public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { if (witnessAgeData == null) { - if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) { + if (CurrencyUtil.isCryptoCurrency(offer.getCounterCurrencyCode())) { witnessAgeData = new WitnessAgeData(WitnessAgeData.TYPE_CRYPTOS); - } else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { + } else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode())) { // Fiat and signed witness required Optional optionalWitness = accountAgeWitnessService.findWitness(offer); AccountAgeWitnessService.SignState signState = optionalWitness diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index 0248b9c522..b2ce32149b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -91,7 +91,6 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import javafx.util.Callback; @@ -179,29 +178,29 @@ abstract public class OfferBookView> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); paymentMethodComboBox.setPrefWidth(250); - - matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null); - matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); - matchingOffersToggleButton.setPrefHeight(27); - Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); - Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); + paymentMethodComboBox.getStyleClass().add("input-with-border"); noDepositOffersToggleButton = new ToggleButton(Res.get("offerbook.filterNoDeposit")); noDepositOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); - noDepositOffersToggleButton.setPrefHeight(27); Tooltip noDepositOffersTooltip = new Tooltip(Res.get("offerbook.noDepositOffers")); Tooltip.install(noDepositOffersToggleButton, noDepositOffersTooltip); + matchingOffersToggleButton = AwesomeDude.createIconToggleButton(AwesomeIcon.USER, null, "1.5em", null); + matchingOffersToggleButton.getStyleClass().add("toggle-button-no-slider"); + Tooltip matchingOffersTooltip = new Tooltip(Res.get("offerbook.matchingOffers")); + Tooltip.install(matchingOffersToggleButton, matchingOffersTooltip); + createOfferButton = new AutoTooltipButton(""); createOfferButton.setMinHeight(40); createOfferButton.setGraphicTextGap(10); - createOfferButton.setStyle("-fx-padding: 0 15 0 15;"); + createOfferButton.setStyle("-fx-padding: 7 25 7 25;"); disabledCreateOfferButtonTooltip = new Label(""); disabledCreateOfferButtonTooltip.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); disabledCreateOfferButtonTooltip.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); @@ -211,15 +210,17 @@ abstract public class OfferBookView autoToolTipTextField = addTopLabelAutoToolTipTextField(""); VBox filterBox = autoToolTipTextField.first; filterInputField = autoToolTipTextField.third; - filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); + filterInputField.setPromptText(Res.get("shared.filter")); + filterInputField.getStyleClass().add("input-with-border"); offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, - filterBox, matchingOffersToggleButton, noDepositOffersToggleButton, getSpacer(), createOfferButtonStack); + filterBox, noDepositOffersToggleButton, matchingOffersToggleButton, getSpacer(), createOfferVBox); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); @@ -228,6 +229,7 @@ abstract public class OfferBookView(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, ++gridRow); GridPane.setColumnIndex(tableView, 0); @@ -261,7 +263,7 @@ abstract public class OfferBookView CurrencyUtil.getCurrencyPair(o.getOffer().getCurrencyCode()), + o -> CurrencyUtil.getCurrencyPair(o.getOffer().getCounterCurrencyCode()), Comparator.nullsFirst(Comparator.naturalOrder()) )); @@ -345,7 +347,7 @@ abstract public class OfferBookView { if (currencyComboBox.getEditor().getText().isEmpty()) - currencyComboBox.getSelectionModel().select(SHOW_ALL); + return; model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods()); model.updateSelectedPaymentMethod(); @@ -405,14 +407,12 @@ abstract public class OfferBookView asString(item).equals(query)). findAny().orElse(null); @@ -511,7 +511,7 @@ abstract public class OfferBookView account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); + offer.getCounterCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); new Popup() .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(tradeLimit, true), @@ -750,8 +749,8 @@ abstract public class OfferBookView { log.debug(Res.get("offerbook.removeOffer.success")); if (DontShowAgainLookup.showAgain(key)) - new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) - .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") + new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("funds.tab.withdrawal"))) + .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); @@ -769,7 +768,7 @@ abstract public class OfferBookView { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, accountViewClass); @@ -812,7 +811,7 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -825,7 +824,7 @@ abstract public class OfferBookView { offerDetailsWindow.show(offer); }); @@ -1065,7 +1061,6 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -1116,7 +1111,12 @@ abstract public class OfferBookView onTakeOffer(offer)); button2.setManaged(false); @@ -1179,8 +1183,8 @@ abstract public class OfferBookView new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( new Callback<>() { @@ -1280,8 +1284,8 @@ abstract public class OfferBookView fillCurrencies(); // refresh filter on changes - offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { - filterOffers(); - }); + // TODO: This is removed because it's expensive to re-filter offers for every change (high cpu for many offers). + // This was used to ensure offer list is fully refreshed, but is unnecessary after refactoring OfferBookService to clone offers? + // offerBook.getOfferBookListItems().addListener((ListChangeListener) c -> { + // filterOffers(); + // }); filterItemsListener = c -> { final Optional highestAmountOffer = filteredItems.stream() @@ -434,11 +436,19 @@ abstract class OfferBookViewModel extends ActivatableViewModel { return formatVolume(item.getOffer(), true); } + String getVolumeAmount(OfferBookListItem item) { + return formatVolume(item.getOffer(), true, false); + } + private String formatVolume(Offer offer, boolean decimalAligned) { + return formatVolume(offer, decimalAligned, showAllTradeCurrenciesProperty.get()); + } + + private String formatVolume(Offer offer, boolean decimalAligned, boolean appendCurrencyCode) { Volume offerVolume = offer.getVolume(); Volume minOfferVolume = offer.getMinVolume(); if (offerVolume != null && minOfferVolume != null) { - String postFix = showAllTradeCurrenciesProperty.get() ? " " + offer.getCurrencyCode() : ""; + String postFix = appendCurrencyCode ? " " + offer.getCounterCurrencyCode() : ""; decimalAligned = decimalAligned && !showAllTradeCurrenciesProperty.get(); return VolumeUtil.formatVolume(offer, decimalAligned, maxPlacesForVolume.get()) + postFix; } else { @@ -447,7 +457,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } int getNumberOfDecimalsForVolume(OfferBookListItem item) { - return CurrencyUtil.isVolumeRoundedToNearestUnit(item.getOffer().getCurrencyCode()) ? GUIUtil.NUM_DECIMALS_UNIT : GUIUtil.NUM_DECIMALS_PRECISE; + return CurrencyUtil.isVolumeRoundedToNearestUnit(item.getOffer().getCounterCurrencyCode()) ? GUIUtil.NUM_DECIMALS_UNIT : GUIUtil.NUM_DECIMALS_PRECISE; } String getPaymentMethod(OfferBookListItem item) { @@ -474,14 +484,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { if (item != null) { Offer offer = item.getOffer(); result = Res.getWithCol("shared.paymentMethod") + " " + Res.get(offer.getPaymentMethod().getId()); - result += "\n" + Res.getWithCol("shared.currency") + " " + CurrencyUtil.getNameAndCode(offer.getCurrencyCode()); - - if (offer.isXmr()) { - String isAutoConf = offer.isXmrAutoConf() ? - Res.get("shared.yes") : - Res.get("shared.no"); - result += "\n" + Res.getWithCol("offerbook.xmrAutoConf") + " " + isAutoConf; - } + result += "\n" + Res.getWithCol("shared.currency") + " " + CurrencyUtil.getNameAndCode(offer.getCounterCurrencyCode()); String countryCode = offer.getCountryCode(); if (isF2F(offer)) { @@ -528,7 +531,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } String getDirectionLabelTooltip(Offer offer) { - return getDirectionWithCodeDetailed(offer.getMirroredDirection(), offer.getCurrencyCode()); + return getDirectionWithCodeDetailed(offer.getMirroredDirection(), offer.getCounterCurrencyCode()); } Optional getMostMaturePaymentAccountForOffer(Offer offer) { @@ -613,9 +616,9 @@ abstract class OfferBookViewModel extends ActivatableViewModel { // filter currencies nextPredicate = nextPredicate.or(offerBookListItem -> { - return offerBookListItem.getOffer().getCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || + return offerBookListItem.getOffer().getCounterCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || offerBookListItem.getOffer().getBaseCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || - CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()) || + CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getCounterCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()) || CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getBaseCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()); }); @@ -681,10 +684,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } private static String getDirectionWithCodeDetailed(OfferDirection direction, String currencyCode) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get("shared.buyingXMRWith", currencyCode) : Res.get("shared.sellingXMRFor", currencyCode); - else - return (direction == OfferDirection.SELL) ? Res.get("shared.buyingCurrency", currencyCode) : Res.get("shared.sellingCurrency", currencyCode); + return (direction == OfferDirection.BUY) ? Res.get("shared.buyingXMRWith", currencyCode) : Res.get("shared.sellingXMRFor", currencyCode); } public String formatDepositString(BigInteger deposit, long amount) { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookViewModel.java index 012af7a822..0d9042e4e5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookViewModel.java @@ -112,8 +112,8 @@ public class OtherOfferBookViewModel extends OfferBookViewModel { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; - boolean currencyResult = CurrencyUtil.isTraditionalCurrency(offer.getCurrencyCode()) && !CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - (showAllTradeCurrenciesProperty.get() || offer.getCurrencyCode().equals(selectedTradeCurrency.getCode())); + boolean currencyResult = CurrencyUtil.isTraditionalCurrency(offer.getCounterCurrencyCode()) && !CurrencyUtil.isFiatCurrency(offer.getCounterCurrencyCode()) && + (showAllTradeCurrenciesProperty.get() || offer.getCounterCurrencyCode().equals(selectedTradeCurrency.getCode())); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.java index f241797fc7..bfc4118d25 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOfferView.java @@ -40,7 +40,6 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; -import javafx.geometry.Insets; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -98,10 +97,6 @@ public class SignedOfferView extends ActivatableViewAndModel isNonZeroPrice = (p) -> p != null && !p.isZero(); + private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -141,7 +146,7 @@ class TakeOfferDataModel extends OfferDataModel { // feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); if (isTabSelected) - priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); if (canTakeOffer()) { tradeManager.checkOfferAvailability(offer, @@ -183,7 +188,7 @@ class TakeOfferDataModel extends OfferDataModel { checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()"); paymentAccount = getLastSelectedPaymentAccount(); - this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); + this.amount.set(getMaxTradeLimit()); updateSecurityDeposit(); @@ -199,7 +204,7 @@ class TakeOfferDataModel extends OfferDataModel { offer.resetState(); - priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } // We don't want that the fee gets updated anymore after we show the funding screen. @@ -210,7 +215,7 @@ class TakeOfferDataModel extends OfferDataModel { void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (isTabSelected) - priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCounterCurrencyCode()); } public void onClose(boolean removeOffer) { @@ -254,7 +259,7 @@ class TakeOfferDataModel extends OfferDataModel { fundsNeededForTrade = fundsNeededForTrade.add(amount.get()); String errorMsg = null; - if (filterManager.isCurrencyBanned(offer.getCurrencyCode())) { + if (filterManager.isCurrencyBanned(offer.getCounterCurrencyCode())) { errorMsg = Res.get("offerbook.warning.currencyBanned"); } else if (filterManager.isPaymentMethodBanned(offer.getPaymentMethod())) { errorMsg = Res.get("offerbook.warning.paymentMethodBanned"); @@ -293,7 +298,7 @@ class TakeOfferDataModel extends OfferDataModel { if (paymentAccount != null) { this.paymentAccount = paymentAccount; - this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); + this.amount.set(getMaxTradeLimit()); preferences.setTakeOfferSelectedPaymentAccountId(paymentAccount.getId()); } @@ -317,6 +322,10 @@ class TakeOfferDataModel extends OfferDataModel { return offer; } + ReadOnlyObjectProperty getVolume() { + return volume; + } + ObservableList getPossiblePaymentAccounts() { Set paymentAccounts = user.getPaymentAccounts(); checkNotNull(paymentAccounts, "paymentAccounts must not be null"); @@ -338,17 +347,17 @@ class TakeOfferDataModel extends OfferDataModel { .orElse(firstItem); } - long getMyMaxTradeLimit() { + BigInteger getMyMaxTradeLimit() { if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), - offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); + return BigInteger.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), + offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())); } else { - return 0; + return BigInteger.ZERO; } } - long getMaxTradeLimit() { - return Math.min(offer.getAmount().longValueExact(), getMyMaxTradeLimit()); + BigInteger getMaxTradeLimit() { + return offer.getAmount().min(getMyMaxTradeLimit()); } boolean canTakeOffer() { @@ -387,8 +396,34 @@ class TakeOfferDataModel extends OfferDataModel { } } + void calculateAmount() { + if (isNonZeroPrice.test(tradePrice) && isNonZeroVolume.test(volume) && allowAmountUpdate) { + try { + Volume volumeBefore = volume.get(); + calculateVolume(); + + // if the volume != amount * price, we need to adjust the amount + if (amount.get() == null || !volumeBefore.equals(tradePrice.getVolumeByAmount(amount.get()))) { + BigInteger value = tradePrice.getAmountByVolume(volumeBefore); + value = value.min(offer.getAmount()); // adjust if above maximum + value = value.max(offer.getMinAmount()); // adjust if below minimum + value = CoinUtil.getRoundedAmount(value, tradePrice, offer.getMinAmount(), getMaxTradeLimit(), offer.getCounterCurrencyCode(), paymentAccount.getPaymentMethod().getId()); + amount.set(value); + } + + calculateTotalToPay(); + } catch (Throwable t) { + log.error(t.toString()); + } + } + } + + protected void setVolume(Volume volume) { + this.volume.set(volume); + } + void maybeApplyAmount(BigInteger amount) { - if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(BigInteger.valueOf(getMaxTradeLimit())) <= 0) { + if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(getMaxTradeLimit()) <= 0) { this.amount.set(amount); } calculateTotalToPay(); @@ -466,11 +501,11 @@ class TakeOfferDataModel extends OfferDataModel { } public String getCurrencyCode() { - return offer.getCurrencyCode(); + return offer.getCounterCurrencyCode(); } public String getCurrencyNameAndCode() { - return CurrencyUtil.getNameByCode(offer.getCurrencyCode()); + return CurrencyUtil.getNameByCode(offer.getCounterCurrencyCode()); } @NotNull diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index dc11216f53..bb807b8a9b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -108,6 +108,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import net.glxn.qrgen.QRCode; @@ -127,13 +128,13 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountsComboBox; private TextArea extraInfoTextArea; private Label amountDescriptionLabel, @@ -142,10 +143,10 @@ public class TakeOfferView extends ActivatableViewAndModel missingCoinListener; @@ -168,7 +170,7 @@ public class TakeOfferView extends ActivatableViewAndModel amountFocusedListener, getShowWalletFundedNotificationListener; + private ChangeListener amountFocusedListener, volumeFocusedListener, getShowWalletFundedNotificationListener; private InfoInputTextField volumeInfoTextField; @@ -200,27 +202,12 @@ public class TakeOfferView extends ActivatableViewAndModel { - model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); - amountTextField.setText(model.amount.get()); - }; - - getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { - if (newValue) { - Notification walletFundedNotification = new Notification() - .headLine(Res.get("notification.walletUpdate.headline")) - .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) - .autoClose(); - - walletFundedNotification.show(); - } - }; GUIUtil.focusWhenAddedToScene(amountTextField); } @@ -339,8 +326,11 @@ public class TakeOfferView extends ActivatableViewAndModel extraInfoTuple = addCompactTopLabelTextArea(gridPane, ++gridRowNoFundingRequired, Res.get("payment.shared.extraInfo.noDeposit"), ""); + extraInfoLabel = extraInfoTuple.first; + extraInfoLabel.setVisible(false); + extraInfoLabel.setManaged(false); + extraInfoTextArea = extraInfoTuple.second; + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); + extraInfoTextArea.setText(offer.getCombinedExtraInfo().trim()); + extraInfoTextArea.getStyleClass().addAll("text-area", "flat-text-area-with-border"); extraInfoTextArea.setWrapText(true); - extraInfoTextArea.setPrefHeight(75); - extraInfoTextArea.setMinHeight(75); - extraInfoTextArea.setMaxHeight(150); + extraInfoTextArea.setMaxHeight(300); extraInfoTextArea.setEditable(false); - GridPane.setRowIndex(extraInfoTextArea, lastGridRowNoFundingRequired); + GUIUtil.adjustHeightAutomatically(extraInfoTextArea); + GridPane.setRowIndex(extraInfoTextArea, gridRowNoFundingRequired); GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); // move up take offer buttons - GridPane.setRowIndex(takeOfferBox, lastGridRowNoFundingRequired + 1); + GridPane.setRowIndex(takeOfferBox, gridRowNoFundingRequired + 1); GridPane.setMargin(takeOfferBox, new Insets(15, 0, 0, 0)); } } @@ -490,19 +486,33 @@ public class TakeOfferView extends ActivatableViewAndModel CurrencyUtil.getCounterCurrency(model.dataModel.getCurrencyCode()))); priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); nextButton.disableProperty().bind(model.isNextButtonDisabled); @@ -631,7 +642,7 @@ public class TakeOfferView extends ActivatableViewAndModel new Popup().warning(newValue + "\n\n" + Res.get("takeOffer.alreadyPaidInFunds")) - .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") + .actionButtonTextWithGoTo("funds.tab.withdrawal") .onAction(() -> { errorPopupDisplayed.set(true); model.resetOfferWarning(); @@ -649,7 +660,7 @@ public class TakeOfferView extends ActivatableViewAndModel { if (newValue != null) { - new Popup().warning(Res.get("takeOffer.error.message", model.errorMessage.get())) + new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get())) .onClose(() -> { errorPopupDisplayed.set(true); model.resetErrorMessage(); @@ -663,6 +674,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + showWarningInvalidXmrDecimalPlacesSubscription = EasyBind.subscribe(model.showWarningInvalidXmrDecimalPlaces, newValue -> { if (newValue) { new Popup().warning(Res.get("takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces")).show(); - model.showWarningInvalidBtcDecimalPlaces.set(false); + model.showWarningInvalidXmrDecimalPlaces.set(false); } }); @@ -693,7 +705,7 @@ public class TakeOfferView extends ActivatableViewAndModel new Popup().headLine(Res.get("takeOffer.success.headline")) .feedback(Res.get("takeOffer.success.info")) - .actionButtonTextWithGoTo("navigation.portfolio.pending") + .actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .dontShowAgainId(key) .onAction(() -> { UserThread.runAfter( @@ -717,13 +729,31 @@ public class TakeOfferView extends ActivatableViewAndModel { + model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); + amountTextField.setText(model.amount.get()); + }; + getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> { + if (newValue) { + Notification walletFundedNotification = new Notification() + .headLine(Res.get("notification.walletUpdate.headline")) + .notification(Res.get("notification.walletUpdate.msg", HavenoUtils.formatXmr(model.dataModel.getTotalToPay().get(), true))) + .autoClose(); + + walletFundedNotification.show(); + } + }; + volumeFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutVolumeTextField(oldValue, newValue); + volumeTextField.setText(model.volume.get()); + }; missingCoinListener = (observable, oldValue, newValue) -> { if (!newValue.toString().equals("")) { updateQrCode(); @@ -733,12 +763,14 @@ public class TakeOfferView extends ActivatableViewAndModel tuple = add2ButtonsWithBox(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel"), 15, true); - buttonBox = tuple.third; + nextButtonBox = tuple.third; nextButton = tuple.first; nextButton.setMaxWidth(200); @@ -870,7 +902,7 @@ public class TakeOfferView extends ActivatableViewAndModel UserThread.runAfter( + Tuple2 qrCodeTuple = GUIUtil.getSmallXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + qrCodeImageView = qrCodeTuple.second; + + Tooltip.install(qrCodePane, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodePane.setOnMouseClicked(e -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(), 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setRowSpan(qrCodeImageView, 3); - GridPane.setValignment(qrCodeImageView, VPos.BOTTOM); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); + GridPane.setRowIndex(qrCodePane, gridRow); + GridPane.setColumnIndex(qrCodePane, 1); + GridPane.setRowSpan(qrCodePane, 3); + GridPane.setValignment(qrCodePane, VPos.BOTTOM); + GridPane.setMargin(qrCodePane, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 10)); + gridPane.getChildren().add(qrCodePane); + + qrCodePane.setVisible(false); + qrCodePane.setManaged(false); addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletAddress")); addressTextField.setVisible(false); + addressTextField.setManaged(false); balanceTextField = addBalanceTextField(gridPane, ++gridRow, Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); + balanceTextField.setManaged(false); fundingHBox = new HBox(); fundingHBox.setVisible(false); @@ -1000,6 +1040,7 @@ public class TakeOfferView extends ActivatableViewAndModel { new GenericMessageWindow() .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getCombinedExtraInfo()) + .instruction(offer.getCombinedExtraInfo().trim()) .actionButtonText(Res.get("shared.iConfirm")) .closeButtonText(Res.get("shared.close")) .width(Layout.INITIAL_WINDOW_WIDTH) @@ -1225,12 +1266,6 @@ public class TakeOfferView extends ActivatableViewAndModel im private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final CoinFormatter xmrFormatter; + private final FiatVolumeValidator fiatVolumeValidator; + private final AmountValidator4Decimals amountValidator4Decimals; + private final AmountValidator8Decimals amountValidator8Decimals; private String amountRange; private String paymentLabel; - private boolean takeOfferRequested; + private boolean takeOfferRequested, ignoreVolumeStringListener; private Trade trade; - private Offer offer; + protected Offer offer; private String price; private String amountDescription; @@ -101,15 +110,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel im final BooleanProperty isTakeOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); + final BooleanProperty showWarningInvalidXmrDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty takeOfferCompleted = new SimpleBooleanProperty(); final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStrListener; private ChangeListener amountListener; + private ChangeListener volumeStringListener; + private ChangeListener volumeListener; private ChangeListener isWalletFundedListener; private ChangeListener tradeStateListener; private ChangeListener offerStateListener; @@ -124,6 +136,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel im @Inject public TakeOfferViewModel(TakeOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + AmountValidator4Decimals amountValidator4Decimals, + AmountValidator8Decimals amountValidator8Decimals, OfferUtil offerUtil, XmrValidator btcValidator, P2PService p2PService, @@ -138,6 +153,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel im this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.xmrFormatter = btcFormatter; + this.fiatVolumeValidator = fiatVolumeValidator; + this.amountValidator4Decimals = amountValidator4Decimals; + this.amountValidator8Decimals = amountValidator8Decimals; createListeners(); } @@ -146,10 +164,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im addBindings(); addListeners(); - String buyVolumeDescriptionKey = offer.isTraditionalOffer() ? "createOffer.amountPriceBox.buy.volumeDescription" : - "createOffer.amountPriceBox.buy.volumeDescriptionCrypto"; - String sellVolumeDescriptionKey = offer.isTraditionalOffer() ? "createOffer.amountPriceBox.sell.volumeDescription" : - "createOffer.amountPriceBox.sell.volumeDescriptionCrypto"; + String buyVolumeDescriptionKey = "createOffer.amountPriceBox.buy.volumeDescription"; + String sellVolumeDescriptionKey = "createOffer.amountPriceBox.sell.volumeDescription"; if (dataModel.getDirection() == OfferDirection.SELL) { volumeDescriptionLabel.set(Res.get(buyVolumeDescriptionKey, dataModel.getCurrencyCode())); @@ -208,8 +224,10 @@ class TakeOfferViewModel extends ActivatableWithDataModel im errorMessage.set(offer.getErrorMessage()); xmrValidator.setMaxValue(offer.getAmount()); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); xmrValidator.setMinValue(offer.getMinAmount()); + + setVolumeToModel(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -237,7 +255,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + xmrValidator.setMaxTradeLimit(dataModel.getMaxTradeLimit()); updateButtonDisableState(); } @@ -256,7 +274,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im new Popup().warning(Res.get("shared.notEnoughFunds", HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), HavenoUtils.formatXmr(dataModel.getTotalAvailableBalance(), true))) - .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .actionButtonTextWithGoTo("funds.tab.deposit") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); return false; @@ -288,7 +306,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im InputValidator.ValidationResult result = isXmrInputValid(amount.get()); amountValidationResult.set(result); if (result.isValid) { - showWarningInvalidBtcDecimalPlaces.set(!DisplayUtils.hasBtcValidDecimals(userInput, xmrFormatter)); + if (userInput != null) showWarningInvalidXmrDecimalPlaces.set(!DisplayUtils.hasBtcValidDecimals(userInput, xmrFormatter)); // only allow max 4 decimal places for xmr values setAmountToModel(); // reformat input @@ -297,12 +315,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel im calculateVolume(); Price tradePrice = dataModel.tradePrice; - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger minAmount = dataModel.getOffer().getMinAmount(); + BigInteger maxAmount = dataModel.getMaxTradeLimit(); if (PaymentMethod.isRoundedForAtmCash(dataModel.getPaymentMethod().getId())) { - BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit); + BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount); dataModel.maybeApplyAmount(adjustedAmountForAtm); - } else if (dataModel.getOffer().isTraditionalOffer()) { - BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { + BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); dataModel.maybeApplyAmount(roundedAmount); } amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); @@ -337,6 +356,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } } + void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); + volumeValidationResult.set(result); + if (result.isValid) { + setVolumeToModel(); + ignoreVolumeStringListener = true; + + Volume volume = dataModel.getVolume().get(); + if (volume != null) { + volume = VolumeUtil.getAdjustedVolume(volume, offer.getPaymentMethod().getId()); + this.volume.set(VolumeUtil.formatVolume(volume)); + } + + ignoreVolumeStringListener = false; + + dataModel.calculateAmount(); + + if (amount.get() != null) + amountValidationResult.set(isXmrInputValid(amount.get())); + } + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// @@ -453,14 +496,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel im /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { - volume.bind(createStringBinding(() -> VolumeUtil.formatVolume(dataModel.volume.get()), dataModel.volume)); totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.getTotalToPay().get(), true), dataModel.getTotalToPay())); } private void removeBindings() { volumeDescriptionLabel.unbind(); - volume.unbind(); totalToPay.unbind(); } @@ -474,10 +515,33 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } updateButtonDisableState(); }; + amountListener = (ov, oldValue, newValue) -> { amount.set(HavenoUtils.formatXmr(newValue)); applyTakerFee(); }; + + volumeStringListener = (ov, oldValue, newValue) -> { + if (!ignoreVolumeStringListener) { + if (isVolumeInputValid(newValue).isValid) { + setVolumeToModel(); + dataModel.calculateAmount(); + dataModel.calculateTotalToPay(); + } + updateButtonDisableState(); + } + }; + + volumeListener = (ov, oldValue, newValue) -> { + ignoreVolumeStringListener = true; + if (newValue != null) + volume.set(VolumeUtil.formatVolume(newValue)); + else + volume.set(""); + + ignoreVolumeStringListener = false; + }; + isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(); @@ -526,9 +590,11 @@ class TakeOfferViewModel extends ActivatableWithDataModel im // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount // We do volume/amount calculation during input, so user has immediate feedback amount.addListener(amountStrListener); + volume.addListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); + dataModel.getVolume().addListener(volumeListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); p2PService.getNetworkNode().addConnectionListener(connectionListener); @@ -540,9 +606,11 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private void removeListeners() { amount.removeListener(amountStrListener); + volume.removeListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); + dataModel.getVolume().removeListener(volumeListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); if (offer != null) { @@ -568,19 +636,49 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); - long maxTradeLimit = dataModel.getMaxTradeLimit(); + BigInteger minAmount = dataModel.getOffer().getMinAmount(); + BigInteger maxAmount = dataModel.getMaxTradeLimit(); Price price = dataModel.tradePrice; if (price != null) { if (dataModel.isRoundedForAtmCash()) { - amount = CoinUtil.getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (dataModel.getOffer().isTraditionalOffer()) { - amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + amount = CoinUtil.getRoundedAtmCashAmount(amount, price, minAmount, maxAmount); + } else if (dataModel.getOffer().isTraditionalOffer() && dataModel.getOffer().isRange()) { + amount = CoinUtil.getRoundedAmount(amount, price, minAmount, maxAmount, dataModel.getOffer().getCounterCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); } } dataModel.maybeApplyAmount(amount); } } + private void setVolumeToModel() { + if (volume.get() != null && !volume.get().isEmpty()) { + try { + dataModel.setVolume(Volume.parse(volume.get(), offer.getCounterCurrencyCode())); + } catch (Throwable t) { + log.debug(t.getMessage()); + } + } else { + dataModel.setVolume(null); + } + } + + private InputValidator.ValidationResult isVolumeInputValid(String input) { + return getVolumeValidator().validate(input); + } + + // TODO: replace with VolumeUtils? + + private MonetaryValidator getVolumeValidator() { + final String code = offer.getCounterCurrencyCode(); + if (CurrencyUtil.isFiatCurrency(code)) { + return fiatVolumeValidator; + } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(code)) { + return amountValidator4Decimals; + } else { + return amountValidator8Decimals; + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -595,7 +693,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public boolean isSellingToAnUnsignedAccount(Offer offer) { if (offer.getDirection() == OfferDirection.BUY && - PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { + PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCounterCurrencyCode())) { // considered risky when either UNSIGNED, PEER_INITIAL, or BANNED (see #5343) return accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.UNSIGNED || accountAgeWitnessService.getSignState(offer) == AccountAgeWitnessService.SignState.PEER_INITIAL || @@ -692,14 +790,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } String getPercentagePriceDescription() { - if (dataModel.isBuyOffer()) { - return dataModel.isCryptoCurrency() ? - Res.get("shared.aboveInPercent") : - Res.get("shared.belowInPercent"); - } else { - return dataModel.isCryptoCurrency() ? - Res.get("shared.belowInPercent") : - Res.get("shared.aboveInPercent"); - } + return dataModel.isBuyOffer() ? Res.get("shared.belowInPercent") : Res.get("shared.aboveInPercent"); } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java index e216b14ed9..d7763bc72c 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -19,6 +19,8 @@ package haveno.desktop.main.overlays; import com.google.common.reflect.TypeToken; import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; @@ -42,8 +44,6 @@ import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; @@ -52,6 +52,7 @@ import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.PerspectiveCamera; import javafx.scene.Scene; @@ -122,7 +123,7 @@ public abstract class Overlay> { Notification(AnimationType.SlideFromRightTop, ChangeBackgroundType.BlurLight), BackgroundInfo(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurUltraLight), - Feedback(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.Darken), + Feedback(AnimationType.SlideDownFromCenterTop, ChangeBackgroundType.BlurLight), Information(AnimationType.FadeInAtCenter, ChangeBackgroundType.BlurLight), Instruction(AnimationType.ScaleFromCenter, ChangeBackgroundType.BlurLight), @@ -141,6 +142,9 @@ public abstract class Overlay> { } } + private static int numCenterOverlays = 0; + private static int numBlurEffects = 0; + protected final static double DEFAULT_WIDTH = 668; protected Stage stage; protected GridPane gridPane; @@ -168,7 +172,7 @@ public abstract class Overlay> { protected boolean showScrollPane = false; protected TextArea messageTextArea; - protected Label headlineIcon, copyIcon, headLineLabel; + protected Label headlineIcon, copyLabel, headLineLabel; protected String headLine, message, closeButtonText, actionButtonText, secondaryActionButtonText, dontShowAgainId, dontShowAgainText, truncatedMessage; @@ -249,6 +253,7 @@ public abstract class Overlay> { protected void animateHide() { animateHide(() -> { + if (isCentered()) numCenterOverlays--; removeEffectFromBackground(); if (stage != null) @@ -500,6 +505,7 @@ public abstract class Overlay> { gridPane.setVgap(5); gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); + gridPane.setMaxHeight(Layout.MAX_POPUP_HEIGHT); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHalignment(HPos.RIGHT); @@ -541,6 +547,14 @@ public abstract class Overlay> { layout(); + // add dropshadow if light mode or multiple centered overlays + if (isCentered()) { + numCenterOverlays++; + } + if (!CssTheme.isDarkTheme() || numCenterOverlays > 1) { + getRootContainer().getStyleClass().add("popup-dropshadow"); + } + addEffectToBackground(); // On Linux the owner stage does not move the child stage as it does on Mac @@ -739,6 +753,8 @@ public abstract class Overlay> { } protected void addEffectToBackground() { + numBlurEffects++; + if (numBlurEffects > 1) return; if (type.changeBackgroundType == ChangeBackgroundType.BlurUltraLight) MainView.blurUltraLight(); else if (type.changeBackgroundType == ChangeBackgroundType.BlurLight) @@ -758,12 +774,14 @@ public abstract class Overlay> { if (headLineLabel != null) { - if (copyIcon != null) { - copyIcon.getStyleClass().add("popup-icon-information"); - copyIcon.setManaged(true); - copyIcon.setVisible(true); - FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em"); - copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> { + if (copyLabel != null) { + copyLabel.getStyleClass().add("popup-icon-information"); + copyLabel.setManaged(true); + copyLabel.setVisible(true); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); + copyLabel.setGraphic(copyIcon); + copyLabel.setCursor(Cursor.HAND); + copyLabel.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); @@ -808,6 +826,8 @@ public abstract class Overlay> { } protected void removeEffectFromBackground() { + numBlurEffects--; + if (numBlurEffects > 0) return; MainView.removeEffect(); } @@ -828,15 +848,15 @@ public abstract class Overlay> { headLineLabel.setStyle(headlineStyle); if (message != null) { - copyIcon = new Label(); - copyIcon.setManaged(false); - copyIcon.setVisible(false); - copyIcon.setPadding(new Insets(3)); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + copyLabel = new Label(); + copyLabel.setManaged(false); + copyLabel.setVisible(false); + copyLabel.setPadding(new Insets(3)); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); final Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); spacer.setMinSize(Layout.PADDING, 1); - hBox.getChildren().addAll(headlineIcon, headLineLabel, spacer, copyIcon); + hBox.getChildren().addAll(headlineIcon, headLineLabel, spacer, copyLabel); } else { hBox.getChildren().addAll(headlineIcon, headLineLabel); } @@ -852,23 +872,8 @@ public abstract class Overlay> { if (message != null) { messageTextArea = new TextArea(truncatedMessage); messageTextArea.setEditable(false); - messageTextArea.getStyleClass().add("text-area-no-border"); - messageTextArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - messageTextArea.applyCss(); - var text = messageTextArea.lookup(".text"); - - messageTextArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return messageTextArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> messageTextArea.requestLayout()); - }); - } - }); + messageTextArea.getStyleClass().add("text-area-popup"); + GUIUtil.adjustHeightAutomatically(messageTextArea); messageTextArea.setWrapText(true); Region messageRegion; @@ -1093,4 +1098,10 @@ public abstract class Overlay> { ", message='" + message + '\'' + '}'; } + + private boolean isCentered() { + if (type.animationType == AnimationType.SlideDownFromCenterTop) return false; + if (type.animationType == AnimationType.SlideFromRightTop) return false; + return true; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java index bd169b3a4b..0af066c9aa 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java @@ -71,7 +71,7 @@ public class PasswordPopup extends Overlay { @Override public void show() { - actionButtonText("CONFIRM"); + actionButtonText("Confirm"); createGridPane(); addHeadLine(); addContent(); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java index 57a9550e3a..ebfcb4dcd4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/Notification.java @@ -42,6 +42,7 @@ public class Notification extends Overlay { private boolean hasBeenDisplayed; private boolean autoClose; private Timer autoCloseTimer; + private static final int BORDER_PADDING = 10; public Notification() { width = 413; // 320 visible bg because of insets @@ -205,8 +206,8 @@ public class Notification extends Overlay { Window window = owner.getScene().getWindow(); double titleBarHeight = window.getHeight() - owner.getScene().getHeight(); double shadowInset = 44; - stage.setX(Math.round(window.getX() + window.getWidth() + shadowInset - stage.getWidth())); - stage.setY(Math.round(window.getY() + titleBarHeight - shadowInset)); + stage.setX(Math.round(window.getX() + window.getWidth() + shadowInset - stage.getWidth() - BORDER_PADDING)); + stage.setY(Math.round(window.getY() + titleBarHeight - shadowInset + BORDER_PADDING)); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java index 829a0640d5..6af7a30601 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/notifications/NotificationCenter.java @@ -205,8 +205,12 @@ public class NotificationCenter { message = Res.get("notification.trade.accepted", role); } - if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) - message = Res.get("notification.trade.unlocked"); + if (trade instanceof BuyerTrade) { + if (phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) + message = Res.get("notification.trade.unlocked"); + else if (phase.ordinal() == Trade.Phase.DEPOSITS_FINALIZED.ordinal()) + message = Res.get("notification.trade.finalized", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED); + } else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal()) message = Res.get("notification.trade.paymentSent"); } @@ -216,7 +220,7 @@ public class NotificationCenter { if (DontShowAgainLookup.showAgain(key)) { Notification notification = new Notification().tradeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(PendingTradesView.class)) { - notification.actionButtonTextWithGoTo("navigation.portfolio.pending") + notification.actionButtonTextWithGoTo("portfolio.tab.pendingTrades") .onAction(() -> { DontShowAgainLookup.dontShowAgain(key, true); navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class); @@ -318,7 +322,7 @@ public class NotificationCenter { private void goToSupport(Trade trade, String message, Class viewClass) { Notification notification = new Notification().disputeHeadLine(trade.getShortId()).message(message); if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(viewClass)) { - notification.actionButtonTextWithGoTo("navigation.support") + notification.actionButtonTextWithGoTo("mainView.menu.support") .onAction(() -> navigation.navigateTo(MainView.class, SupportView.class, viewClass)) .show(); } else { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/popups/PopupManager.java b/desktop/src/main/java/haveno/desktop/main/overlays/popups/PopupManager.java index ed354b4f9a..151a8d95f6 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/popups/PopupManager.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/popups/PopupManager.java @@ -32,10 +32,15 @@ public class PopupManager { private static Popup displayedPopup; public static void queueForDisplay(Popup popup) { + if (hasDuplicatePopup(popup)) { + log.warn("The popup is already in the queue or displayed.\n\t" + + "New popup not added=" + popup); + return; + } boolean result = popups.offer(popup); if (!result) log.warn("The capacity is full with popups in the queue.\n\t" + - "Not added new popup=" + popup); + "New popup not added=" + popup); displayNext(); } @@ -57,4 +62,16 @@ public class PopupManager { } } } + + private static boolean hasDuplicatePopup(Popup popup) { + if (displayedPopup != null && displayedPopup.toString().equals(popup.toString())) { + return true; + } + for (Popup p : popups) { + if (p.toString().equals(popup.toString())) { + return true; + } + } + return false; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java index 6a238e56ee..4bc6add0b0 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java @@ -46,6 +46,7 @@ import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextField; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelExplorerAddressTextField; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; +import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; @@ -137,15 +138,20 @@ public class ContractWindow extends Overlay { addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("contractWindow.title")); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), Layout.TWICE_FIRST_ROW_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.dates"), DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), DisplayUtils.getDirectionBothSides(offer.getDirection(), offer.isPrivateOffer())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), HavenoUtils.formatXmr(contract.getTradeAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode, ":"), @@ -157,16 +163,20 @@ public class ContractWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(contract.getTradeAmount()), true); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.xmrAddresses"), contract.getBuyerPayoutAddressString() + " / " + contract.getSellerPayoutAddressString()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.onions"), contract.getBuyerNodeAddress().getFullAddress() + " / " + contract.getSellerNodeAddress().getFullAddress()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.accountAge"), @@ -176,16 +186,19 @@ public class ContractWindow extends Overlay { DisputeManager> disputeManager = getDisputeManager(dispute); String nrOfDisputesAsBuyer = disputeManager != null ? disputeManager.getNrOfDisputes(true, contract) : ""; String nrOfDisputesAsSeller = disputeManager != null ? disputeManager.getNrOfDisputes(false, contract) : ""; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), dispute.getBuyerPaymentAccountPayload() != null ? dispute.getBuyerPaymentAccountPayload().getPaymentDetails() : "NA"); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), @@ -219,6 +232,7 @@ public class ContractWindow extends Overlay { NodeAddress agentNodeAddress = disputeManager.getAgentNodeAddress(dispute); if (agentNodeAddress != null) { String value = agentMatrixUserName + " (" + agentNodeAddress.getFullAddress() + ")"; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, title, value); } } @@ -232,40 +246,53 @@ public class ContractWindow extends Overlay { countries = CountryUtil.getCodesString(acceptedCountryCodes); tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedTakerCountries"), countries) .second.setTooltip(tooltip); } if (showAcceptedBanks) { if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.bankName"), acceptedBanks.get(0)); } else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip(Res.get("shared.acceptedBanks") + value); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.acceptedBanks"), value) .second.setTooltip(tooltip); } } + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), contract.getMakerDepositTxHash()); - if (contract.getTakerDepositTxHash() != null) + if (contract.getTakerDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); + } - if (dispute.getDelayedPayoutTxId() != null) + if (dispute.getDelayedPayoutTxId() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); + } if (dispute.getDonationAddressOfDelayedPayoutTx() != null) { + addSeparator(gridPane, ++rowIndex); addLabelExplorerAddressTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), dispute.getDonationAddressOfDelayedPayoutTx()); } - if (dispute.getPayoutTxSerialized() != null) + if (dispute.getPayoutTxSerialized() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId()); + } - if (dispute.getContractHash() != null) + if (dispute.getContractHash() != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.contractHash"), Utils.HEX.encode(dispute.getContractHash())).second.setMouseTransparent(false); + } + addSeparator(gridPane, ++rowIndex); Button viewContractButton = addConfirmationLabelButton(gridPane, ++rowIndex, Res.get("shared.contractAsJson"), Res.get("shared.viewContractAsJson"), 0).second; viewContractButton.setDefaultButton(false); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index ebf1c25309..9991638847 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -25,6 +25,7 @@ import haveno.common.handlers.ResultHandler; import haveno.common.util.Tuple2; import haveno.common.util.Tuple3; import haveno.core.api.CoreDisputesService; +import haveno.core.api.CoreDisputesService.PayoutSuggestion; import haveno.core.locale.Res; import haveno.core.support.SupportType; import haveno.core.support.dispute.Dispute; @@ -33,7 +34,6 @@ import haveno.core.support.dispute.DisputeManager; import haveno.core.support.dispute.DisputeResult; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; -import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; @@ -138,6 +138,7 @@ public class DisputeSummaryWindow extends Overlay { public void show(Dispute dispute) { this.dispute = dispute; this.trade = tradeManager.getTrade(dispute.getTradeId()); + this.payoutSuggestion = null; rowIndex = -1; width = 1150; @@ -186,7 +187,7 @@ public class DisputeSummaryWindow extends Overlay { protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); - gridPane.getStyleClass().add("grid-pane"); + gridPane.getStyleClass().addAll("grid-pane", "popup-with-input"); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.setPrefWidth(width); } @@ -219,32 +220,14 @@ public class DisputeSummaryWindow extends Overlay { disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); disputeResult.setSubtractFeeFrom(peersDisputeResult.getSubtractFeeFrom()); - buyerGetsTradeAmountRadioButton.setDisable(true); - buyerGetsAllRadioButton.setDisable(true); - sellerGetsTradeAmountRadioButton.setDisable(true); - sellerGetsAllRadioButton.setDisable(true); - customRadioButton.setDisable(true); - - buyerPayoutAmountInputTextField.setDisable(true); - sellerPayoutAmountInputTextField.setDisable(true); - buyerPayoutAmountInputTextField.setEditable(false); - sellerPayoutAmountInputTextField.setEditable(false); - - reasonWasBugRadioButton.setDisable(true); - reasonWasUsabilityIssueRadioButton.setDisable(true); - reasonProtocolViolationRadioButton.setDisable(true); - reasonNoReplyRadioButton.setDisable(true); - reasonWasScamRadioButton.setDisable(true); - reasonWasOtherRadioButton.setDisable(true); - reasonWasBankRadioButton.setDisable(true); - reasonWasOptionTradeRadioButton.setDisable(true); - reasonWasSellerNotRespondingRadioButton.setDisable(true); - reasonWasWrongSenderAccountRadioButton.setDisable(true); - reasonWasPeerWasLateRadioButton.setDisable(true); - reasonWasTradeAlreadySettledRadioButton.setDisable(true); - - applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get()); + disableTradeAmountPayoutControls(); applyTradeAmountRadioButtonStates(); + } else if (trade.isPayoutPublished()) { + log.warn("Payout is already published for {} {}, disabling payout controls", trade.getClass().getSimpleName(), trade.getId()); + disableTradeAmountPayoutControls(); + } else if (trade.isDepositTxMissing()) { + log.warn("Missing deposit tx for {} {}, disabling some payout controls", trade.getClass().getSimpleName(), trade.getId()); + disableTradeAmountPayoutControlsWhenDepositMissing(); } setReasonRadioButtonState(); @@ -253,6 +236,37 @@ public class DisputeSummaryWindow extends Overlay { addButtons(contract); } + private void disableTradeAmountPayoutControls() { + buyerGetsTradeAmountRadioButton.setDisable(true); + buyerGetsAllRadioButton.setDisable(true); + sellerGetsTradeAmountRadioButton.setDisable(true); + sellerGetsAllRadioButton.setDisable(true); + customRadioButton.setDisable(true); + + buyerPayoutAmountInputTextField.setDisable(true); + sellerPayoutAmountInputTextField.setDisable(true); + buyerPayoutAmountInputTextField.setEditable(false); + sellerPayoutAmountInputTextField.setEditable(false); + + reasonWasBugRadioButton.setDisable(true); + reasonWasUsabilityIssueRadioButton.setDisable(true); + reasonProtocolViolationRadioButton.setDisable(true); + reasonNoReplyRadioButton.setDisable(true); + reasonWasScamRadioButton.setDisable(true); + reasonWasOtherRadioButton.setDisable(true); + reasonWasBankRadioButton.setDisable(true); + reasonWasOptionTradeRadioButton.setDisable(true); + reasonWasSellerNotRespondingRadioButton.setDisable(true); + reasonWasWrongSenderAccountRadioButton.setDisable(true); + reasonWasPeerWasLateRadioButton.setDisable(true); + reasonWasTradeAlreadySettledRadioButton.setDisable(true); + } + + private void disableTradeAmountPayoutControlsWhenDepositMissing() { + buyerGetsTradeAmountRadioButton.setDisable(true); + sellerGetsTradeAmountRadioButton.setDisable(true); + } + private void addInfoPane() { Contract contract = dispute.getContract(); addTitledGroupBg(gridPane, ++rowIndex, 17, Res.get("disputeSummaryWindow.title")).getStyleClass().add("last"); @@ -354,22 +368,11 @@ public class DisputeSummaryWindow extends Overlay { BigInteger sellerAmount = HavenoUtils.parseXmr(sellerPayoutAmountInputTextField.getText()); Contract contract = dispute.getContract(); BigInteger tradeAmount = contract.getTradeAmount(); - BigInteger available = tradeAmount + BigInteger expected = tradeAmount .add(trade.getBuyer().getSecurityDeposit()) .add(trade.getSeller().getSecurityDeposit()); BigInteger totalAmount = buyerAmount.add(sellerAmount); - - boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager; - if (isRefundAgent) { - // We allow to spend less in case of RefundAgent or even zero to both, so in that case no payout tx will - // be made - return totalAmount.compareTo(available) <= 0; - } else { - if (totalAmount.compareTo(BigInteger.ZERO) <= 0) { - return false; - } - return totalAmount.compareTo(available) == 0; - } + return totalAmount.compareTo(expected) == 0 || totalAmount.compareTo(trade.getWallet().getBalance()) == 0; // allow spending the expected amount or full wallet balance in case a deposit transaction was dropped } private void applyCustomAmounts(InputTextField inputTextField, boolean oldFocusValue, boolean newFocusValue) { @@ -379,10 +382,7 @@ public class DisputeSummaryWindow extends Overlay { return; } - Contract contract = dispute.getContract(); - BigInteger available = contract.getTradeAmount() - .add(trade.getBuyer().getSecurityDeposit()) - .add(trade.getSeller().getSecurityDeposit()); + BigInteger available = trade.getWallet().getBalance(); BigInteger enteredAmount = HavenoUtils.parseXmr(inputTextField.getText()); if (enteredAmount.compareTo(available) > 0) { enteredAmount = available; @@ -412,11 +412,13 @@ public class DisputeSummaryWindow extends Overlay { private void addPayoutAmountTextFields() { buyerPayoutAmountInputTextField = new InputTextField(); buyerPayoutAmountInputTextField.setLabelFloat(true); + buyerPayoutAmountInputTextField.getStyleClass().add("label-float"); buyerPayoutAmountInputTextField.setEditable(false); buyerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.buyer")); sellerPayoutAmountInputTextField = new InputTextField(); sellerPayoutAmountInputTextField.setLabelFloat(true); + sellerPayoutAmountInputTextField.getStyleClass().add("label-float"); sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setEditable(false); @@ -590,16 +592,27 @@ public class DisputeSummaryWindow extends Overlay { !trade.isPayoutPublished()) { // create payout tx - MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); + try { + MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); - // show confirmation - showPayoutTxConfirmation(contract, - payoutTx, - () -> doClose(closeTicketButton, cancelButton), - () -> { - closeTicketButton.setDisable(false); - cancelButton.setDisable(false); - }); + // show confirmation + showPayoutTxConfirmation(contract, + payoutTx, + () -> doClose(closeTicketButton, cancelButton), + () -> { + closeTicketButton.setDisable(false); + cancelButton.setDisable(false); + }); + } catch (Exception ex) { + if (trade.isPayoutPublished()) { + doClose(closeTicketButton, cancelButton); + } else { + log.error("Error creating dispute payout tx for dispute: " + ex.getMessage(), ex); + new Popup().error(ex.getMessage()).show(); + closeTicketButton.setDisable(false); + cancelButton.setDisable(false); + } + } } else { doClose(closeTicketButton, cancelButton); } @@ -724,6 +737,10 @@ public class DisputeSummaryWindow extends Overlay { private void applyTradeAmountRadioButtonStates() { + if (payoutSuggestion == null) { + payoutSuggestion = getPayoutSuggestionFromDisputeResult(); + } + BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); @@ -748,4 +765,20 @@ public class DisputeSummaryWindow extends Overlay { break; } } + + // TODO: Persist the payout suggestion to DisputeResult like Bisq upstream? + // That would be a better design, but it's not currently needed. + private PayoutSuggestion getPayoutSuggestionFromDisputeResult() { + if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { + return PayoutSuggestion.SELLER_GETS_ALL; + } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(BigInteger.ZERO)) { + return PayoutSuggestion.BUYER_GETS_ALL; + } else if (disputeResult.getBuyerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getBuyer().getSecurityDeposit()))) { + return PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; + } else if (disputeResult.getSellerPayoutAmountBeforeCost().equals(trade.getAmount().add(trade.getSeller().getSecurityDeposit()))) { + return PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; + } else { + return PayoutSuggestion.CUSTOM; + } + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java index 01d119a479..9ee42ae27c 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/GenericMessageWindow.java @@ -18,6 +18,7 @@ package haveno.desktop.main.overlays.windows; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import javafx.scene.control.Label; import javafx.scene.control.TextArea; @@ -28,6 +29,7 @@ import static haveno.desktop.util.FormBuilder.addTextArea; public class GenericMessageWindow extends Overlay { private String preamble; + private static final double MAX_TEXT_AREA_HEIGHT = 250; public GenericMessageWindow() { super(); @@ -54,20 +56,11 @@ public class GenericMessageWindow extends Overlay { } checkNotNull(message, "message must not be null"); TextArea textArea = addTextArea(gridPane, ++rowIndex, "", 10); + textArea.getStyleClass().add("flat-text-area-with-border"); textArea.setText(message); textArea.setEditable(false); textArea.setWrapText(true); - // sizes the textArea to fit within its parent container - double verticalSizePercentage = ensureRange(countLines(message) / 20.0, 0.2, 0.7); - textArea.setPrefSize(Layout.INITIAL_WINDOW_WIDTH, Layout.INITIAL_WINDOW_HEIGHT * verticalSizePercentage); - } - - private static int countLines(String str) { - String[] lines = str.split("\r\n|\r|\n"); - return lines.length; - } - - private static double ensureRange(double value, double min, double max) { - return Math.min(Math.max(value, min), max); + textArea.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + GUIUtil.adjustHeightAutomatically(textArea, MAX_TEXT_AREA_HEIGHT); } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index dd645246f7..c1dba2b71c 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -20,10 +20,16 @@ package haveno.desktop.main.overlays.windows; import com.google.common.base.Joiner; import com.google.inject.Inject; import com.google.inject.name.Named; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.util.Tuple2; import haveno.common.util.Tuple4; +import haveno.common.util.Utilities; import haveno.core.locale.CountryUtil; import haveno.core.locale.Res; import haveno.core.monetary.Price; @@ -45,30 +51,37 @@ import haveno.desktop.components.BusyAnimation; import haveno.desktop.main.overlays.Overlay; import haveno.desktop.main.overlays.editor.PasswordPopup; import haveno.desktop.main.overlays.popups.Popup; -import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import static haveno.desktop.util.FormBuilder.addButtonAfterGroup; import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; +import static haveno.desktop.util.FormBuilder.addLabel; +import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; import java.math.BigInteger; import java.util.List; import java.util.Optional; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.geometry.HPos; import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; @@ -115,7 +128,7 @@ public class OfferDetailsWindow extends Overlay { this.tradePrice = tradePrice; rowIndex = -1; - width = 1118; + width = 1050; createGridPane(); addContent(); display(); @@ -124,7 +137,7 @@ public class OfferDetailsWindow extends Overlay { public void show(Offer offer) { this.offer = offer; rowIndex = -1; - width = 1118; + width = 1050; createGridPane(); addContent(); display(); @@ -189,17 +202,12 @@ public class OfferDetailsWindow extends Overlay { if (isF2F) rows++; - boolean showXmrAutoConf = offer.isXmr() && offer.getDirection() == OfferDirection.SELL; - if (showXmrAutoConf) { - rows++; - } - - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get(offer.isPrivateOffer() ? "shared.Offer" : "shared.Offer")); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; OfferDirection direction = offer.getDirection(); - String currencyCode = offer.getCurrencyCode(); + String currencyCode = offer.getCounterCurrencyCode(); String offerTypeLabel = Res.get("shared.offerType"); String toReceive = " " + Res.get("shared.toReceive"); String toSpend = " " + Res.get("shared.toSpend"); @@ -218,17 +226,22 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, DisplayUtils.getDirectionBothSides(direction, offer.isPrivateOffer()), firstRowDistance); } + String amount = Res.get("shared.xmrAmount"); + addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(tradeAmount, true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, VolumeUtil.formatVolumeLabel(currencyCode) + counterCurrencyDirectionInfo, - VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); + VolumeUtil.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount, offer.getMinAmount(), tradeAmount))); } else { addConfirmationLabelLabel(gridPane, ++rowIndex, amount + xmrDirectionInfo, HavenoUtils.formatXmr(offer.getAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.minXmrAmount"), HavenoUtils.formatXmr(offer.getMinAmount(), true)); + addSeparator(gridPane, ++rowIndex); String volume = VolumeUtil.formatVolumeWithCode(offer.getVolume()); String minVolume = ""; if (offer.getVolume() != null && offer.getMinVolume() != null && @@ -239,6 +252,7 @@ public class OfferDetailsWindow extends Overlay { } String priceLabel = Res.get("shared.price"); + addSeparator(gridPane, ++rowIndex); if (takeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(tradePrice)); } else { @@ -264,6 +278,7 @@ public class OfferDetailsWindow extends Overlay { final PaymentAccount myPaymentAccount = user.getPaymentAccount(makerPaymentAccountId); String countryCode = offer.getCountryCode(); boolean isMyOffer = offer.isMyOffer(keyRing); + addSeparator(gridPane, ++rowIndex); if (isMyOffer && makerPaymentAccountId != null && myPaymentAccount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.myTradingAccount"), myPaymentAccount.getAccountName()); } else { @@ -271,17 +286,12 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), method); } - if (showXmrAutoConf) { - String isAutoConf = offer.isXmrAutoConf() ? - Res.get("shared.yes") : - Res.get("shared.no"); - addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerbook.xmrAutoConf"), isAutoConf); - } - if (showAcceptedBanks) { if (paymentMethod.equals(PaymentMethod.SAME_BANK)) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.bankId"), acceptedBanks.get(0)); } else if (isSpecificBanks) { + addSeparator(gridPane, ++rowIndex); String value = Joiner.on(", ").join(acceptedBanks); String acceptedBanksLabel = Res.get("shared.acceptedBanks"); Tooltip tooltip = new Tooltip(acceptedBanksLabel + " " + value); @@ -291,6 +301,7 @@ public class OfferDetailsWindow extends Overlay { } } if (showAcceptedCountryCodes) { + addSeparator(gridPane, ++rowIndex); String countries; Tooltip tooltip = null; if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) { @@ -313,29 +324,16 @@ public class OfferDetailsWindow extends Overlay { } if (isF2F) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity()); } if (showOfferExtraInfo) { + addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second; - textArea.setText(offer.getCombinedExtraInfo()); - textArea.setMaxHeight(200); - textArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - textArea.applyCss(); - var text = textArea.lookup(".text"); - - textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return textArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> textArea.requestLayout()); - }); - } - }); + textArea.setText(offer.getCombinedExtraInfo().trim()); + textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); + GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); } // get amount reserved for the offer @@ -355,13 +353,16 @@ public class OfferDetailsWindow extends Overlay { if (offerChallenge != null) rows++; - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"), offer.getMakerNodeAddress().getFullAddress()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.creationDate"), DisplayUtils.formatDateTime(offer.getDate())); + addSeparator(gridPane, ++rowIndex); String value = Res.getWithColAndCap("shared.buyer") + " " + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + @@ -372,27 +373,75 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); if (reservedAmount != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } - if (countryCode != null && !isF2F) + if (countryCode != null && !isF2F) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); + } - if (offerChallenge != null) - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), offerChallenge); + if (offerChallenge != null) { + addSeparator(gridPane, ++rowIndex); + + // add label + Label label = addLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), 0); + label.getStyleClass().addAll("confirmation-label", "regular-text-color"); + GridPane.setHalignment(label, HPos.LEFT); + GridPane.setValignment(label, VPos.TOP); + + // add vbox with passphrase and copy button + VBox vbox = new VBox(13); + vbox.setAlignment(Pos.TOP_CENTER); + VBox.setVgrow(vbox, Priority.ALWAYS); + vbox.getStyleClass().addAll("passphrase-copy-box"); + + // add passphrase + JFXTextField centerLabel = new JFXTextField(offerChallenge); + centerLabel.getStyleClass().add("confirmation-value"); + centerLabel.setAlignment(Pos.CENTER); + centerLabel.setFocusTraversable(false); + + // add copy button + Label copyLabel = new Label(); + copyLabel.getStyleClass().addAll("icon"); + copyLabel.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.2em"); + copyIcon.setFill(Color.WHITE); + copyLabel.setGraphic(copyIcon); + JFXButton copyButton = new JFXButton(Res.get("offerDetailsWindow.challenge.copy"), copyLabel); + copyButton.setContentDisplay(ContentDisplay.LEFT); + copyButton.setGraphicTextGap(8); + copyButton.setOnMouseClicked(e -> { + Utilities.copyToClipboard(offerChallenge); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); + copyButton.setId("buy-button"); + copyButton.setFocusTraversable(false); + vbox.getChildren().addAll(centerLabel, copyButton); + + // add vbox to grid pane in next column + GridPane.setRowIndex(vbox, rowIndex); + GridPane.setColumnIndex(vbox, 1); + gridPane.getChildren().add(vbox); + } if (placeOfferHandlerOptional.isPresent()) { - addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); addConfirmAndCancelButtons(true); } else if (takeOfferHandlerOptional.isPresent()) { - addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.COMPACT_GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.tac"), Res.get("takeOffer.tac"), - Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); labelLabelTuple2.second.setWrapText(true); addConfirmAndCancelButtons(false); @@ -412,14 +461,11 @@ public class OfferDetailsWindow extends Overlay { boolean isBuyOffer = offer.isBuyOffer(); boolean isBuyerRole = isPlaceOffer == isBuyOffer; String placeOfferButtonText = isBuyerRole ? - Res.get("offerDetailsWindow.confirm.maker", Res.get("shared.buy")) : - Res.get("offerDetailsWindow.confirm.maker", Res.get("shared.sell")); + Res.get("offerDetailsWindow.confirm.maker.buy", offer.getCounterCurrencyCode()) : + Res.get("offerDetailsWindow.confirm.maker.sell", offer.getCounterCurrencyCode()); String takeOfferButtonText = isBuyerRole ? - Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.buy")) : - Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.sell")); - - ImageView iconView = new ImageView(); - iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); + Res.get("offerDetailsWindow.confirm.taker.buy", offer.getCounterCurrencyCode()) : + Res.get("offerDetailsWindow.confirm.taker.sell", offer.getCounterCurrencyCode()); Tuple4 placeOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++rowIndex, 1, @@ -428,11 +474,18 @@ public class OfferDetailsWindow extends Overlay { AutoTooltipButton confirmButton = (AutoTooltipButton) placeOfferTuple.first; confirmButton.setMinHeight(40); confirmButton.setPadding(new Insets(0, 20, 0, 20)); - confirmButton.setGraphic(iconView); confirmButton.setGraphicTextGap(10); confirmButton.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); confirmButton.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); + if (offer.hasBuyerAsTakerWithoutDeposit()) { + confirmButton.setGraphic(GUIUtil.getLockLabel()); + } else { + ImageView iconView = new ImageView(); + iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); + confirmButton.setGraphic(iconView); + } + busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java index 8bca62d143..21809a7501 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java @@ -17,9 +17,12 @@ package haveno.desktop.main.overlays.windows; +import haveno.common.util.Tuple2; +import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.GUIUtil; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.control.Label; @@ -27,31 +30,35 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.net.URI; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); - private final ImageView qrCodeImageView; + private final StackPane qrCodePane; private final String moneroUri; - public QRCodeWindow(String bitcoinURI) { - this.moneroUri = bitcoinURI; + public QRCodeWindow(String moneroUri) { + this.moneroUri = moneroUri; + + Tuple2 qrCodeTuple = GUIUtil.getBigXmrQrCodePane(); + qrCodePane = qrCodeTuple.first; + ImageView qrCodeImageView = qrCodeTuple.second; + final byte[] imageBytes = QRCode - .from(bitcoinURI) + .from(moneroUri) .withSize(300, 300) .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); - qrCodeImageView = new ImageView(qrImage); - qrCodeImageView.setFitHeight(250); - qrCodeImageView.setFitWidth(250); - qrCodeImageView.getStyleClass().add("qr-code"); + qrCodeImageView.setImage(qrImage); type = Type.Information; width = 468; @@ -65,10 +72,11 @@ public class QRCodeWindow extends Overlay { addHeadLine(); addMessage(); - GridPane.setRowIndex(qrCodeImageView, ++rowIndex); - GridPane.setColumnSpan(qrCodeImageView, 2); - GridPane.setHalignment(qrCodeImageView, HPos.CENTER); - gridPane.getChildren().add(qrCodeImageView); + qrCodePane.setOnMouseClicked(event -> openWallet()); + GridPane.setRowIndex(qrCodePane, ++rowIndex); + GridPane.setColumnSpan(qrCodePane, 2); + GridPane.setHalignment(qrCodePane, HPos.CENTER); + gridPane.getChildren().add(qrCodePane); String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); @@ -91,4 +99,12 @@ public class QRCodeWindow extends Overlay { public String getClipboardText() { return moneroUri; } + + private void openWallet() { + try { + Utilities.openURI(URI.create(moneroUri)); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java index fe8b8c9009..3bf7be6b14 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/SignPaymentAccountsWindow.java @@ -113,7 +113,7 @@ public class SignPaymentAccountsWindow extends Overlay Button urlButton = new AutoTooltipButton(Res.get("torNetworkSettingWindow.openTorWebPage")); urlButton.setOnAction(event -> { try { - Utilities.openURI(URI.create("https://bridges.torproject.org/bridges")); + Utilities.openURI(URI.create("https://bridges.torproject.org")); } catch (IOException e) { e.printStackTrace(); } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java index 004cc0a3f5..437dc8eaf9 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -38,22 +38,21 @@ import haveno.core.xmr.wallet.BtcWalletService; import haveno.desktop.components.HavenoTextArea; import haveno.desktop.main.MainView; import haveno.desktop.main.overlays.Overlay; -import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; +import haveno.desktop.util.GUIUtil; + import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextField; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; +import static haveno.desktop.util.FormBuilder.addSeparator; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; -import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; @@ -107,7 +106,7 @@ public class TradeDetailsWindow extends Overlay { this.trade = trade; rowIndex = -1; - width = 918; + width = Layout.DETAILS_WINDOW_WIDTH; createGridPane(); addContent(); display(); @@ -127,7 +126,6 @@ public class TradeDetailsWindow extends Overlay { @Override protected void createGridPane() { super.createGridPane(); - gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); } @@ -135,7 +133,7 @@ public class TradeDetailsWindow extends Overlay { Offer offer = trade.getOffer(); Contract contract = trade.getContract(); - int rows = 5; + int rows = 9; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("tradeDetailsWindow.headline")); boolean myOffer = tradeManager.isMyOffer(offer); @@ -146,28 +144,32 @@ public class TradeDetailsWindow extends Overlay { String offerType = Res.get("shared.offerType"); if (tradeManager.isBuyer(offer)) { addConfirmationLabelTextField(gridPane, rowIndex, offerType, - DisplayUtils.getDirectionForBuyer(myOffer, offer.getCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); + DisplayUtils.getDirectionForBuyer(myOffer, offer.getCounterCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); counterCurrencyDirectionInfo = toSpend; xmrDirectionInfo = toReceive; } else { addConfirmationLabelTextField(gridPane, rowIndex, offerType, - DisplayUtils.getDirectionForSeller(myOffer, offer.getCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); + DisplayUtils.getDirectionForSeller(myOffer, offer.getCounterCurrencyCode()), Layout.TWICE_FIRST_ROW_DISTANCE); counterCurrencyDirectionInfo = toReceive; xmrDirectionInfo = toSpend; } + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.xmrAmount") + xmrDirectionInfo, HavenoUtils.formatXmr(trade.getAmount(), true)); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, - VolumeUtil.formatVolumeLabel(offer.getCurrencyCode()) + counterCurrencyDirectionInfo, + VolumeUtil.formatVolumeLabel(offer.getCounterCurrencyCode()) + counterCurrencyDirectionInfo, VolumeUtil.formatVolumeWithCode(trade.getVolume())); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(trade.getPrice())); String paymentMethodText = Res.get(offer.getPaymentMethod().getId()); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), paymentMethodText); // second group - rows = 7; + rows = 5; if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) rows++; @@ -200,9 +202,10 @@ public class TradeDetailsWindow extends Overlay { if (trade.getTradePeerNodeAddress() != null) rows++; - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.COMPACT_GROUP_DISTANCE); addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"), - trade.getId(), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + trade.getId(), Layout.TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeDate"), DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + @@ -212,40 +215,30 @@ public class TradeDetailsWindow extends Overlay { Res.getWithColAndCap("shared.seller") + " " + HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee(), true); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); if (arbitratorNodeAddress != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.agentAddresses"), arbitratorNodeAddress.getFullAddress()); } - if (trade.getTradePeerNodeAddress() != null) + if (trade.getTradePeerNodeAddress() != null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"), trade.getTradePeerNodeAddress().getFullAddress()); + } if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + addSeparator(gridPane, ++rowIndex); TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo.offer"), "", 0).second; - textArea.setText(offer.getCombinedExtraInfo()); - textArea.setMaxHeight(200); - textArea.sceneProperty().addListener((o, oldScene, newScene) -> { - if (newScene != null) { - // avoid javafx css warning - CssTheme.loadSceneStyles(newScene, CssTheme.CSS_THEME_LIGHT, false); - textArea.applyCss(); - var text = textArea.lookup(".text"); - - textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { - return textArea.getFont().getSize() + text.getBoundsInLocal().getHeight(); - }, text.boundsInLocalProperty())); - - text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { - Platform.runLater(() -> textArea.requestLayout()); - }); - } - }); + textArea.setText(offer.getCombinedExtraInfo().trim()); + textArea.setMaxHeight(Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); textArea.setEditable(false); + GUIUtil.adjustHeightAutomatically(textArea, Layout.DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT); } if (contract != null) { @@ -254,6 +247,7 @@ public class TradeDetailsWindow extends Overlay { if (buyerPaymentAccountPayload != null) { String paymentDetails = buyerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + buyersAccountAge; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); @@ -261,21 +255,27 @@ public class TradeDetailsWindow extends Overlay { if (sellerPaymentAccountPayload != null) { String paymentDetails = sellerPaymentAccountPayload.getPaymentDetails(); String postFix = " / " + sellersAccountAge; + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), paymentDetails + postFix).second.setTooltip(new Tooltip(paymentDetails + postFix)); } - if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) + if (buyerPaymentAccountPayload == null && sellerPaymentAccountPayload == null) { + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), Res.get(contract.getPaymentMethodId())); + } } - if (trade.getMaker().getDepositTxHash() != null) + if (trade.getMaker().getDepositTxHash() != null) { + addSeparator(gridPane, ++rowIndex); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), trade.getMaker().getDepositTxHash()); - if (trade.getTaker().getDepositTxHash() != null) + } + if (trade.getTaker().getDepositTxHash() != null) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), trade.getTaker().getDepositTxHash()); + } if (showDisputedTx) { @@ -287,6 +287,7 @@ public class TradeDetailsWindow extends Overlay { } if (trade.hasFailed()) { + addSeparator(gridPane, ++rowIndex); textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; textArea.setText(trade.getErrorMessage()); textArea.setEditable(false); @@ -302,6 +303,7 @@ public class TradeDetailsWindow extends Overlay { textArea.scrollTopProperty().addListener(changeListener); textArea.setScrollTop(30); + addSeparator(gridPane, ++rowIndex); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePhase"), trade.getPhase().name()); } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java index 76d414b380..a9202f9b8a 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TradeFeedbackWindow.java @@ -40,7 +40,7 @@ public class TradeFeedbackWindow extends Overlay { @Override public void show() { headLine(Res.get("tradeFeedbackWindow.title")); - message(Res.get("tradeFeedbackWindow.msg.part1")); + //message(Res.get("tradeFeedbackWindow.msg.part1")); // TODO: this message part has padding which remaining message does not have hideCloseButton(); actionButtonText(Res.get("shared.close")); @@ -51,6 +51,17 @@ public class TradeFeedbackWindow extends Overlay { protected void addMessage() { super.addMessage(); + AutoTooltipLabel messageLabel1 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part1")); + messageLabel1.setMouseTransparent(true); + messageLabel1.setWrapText(true); + GridPane.setHalignment(messageLabel1, HPos.LEFT); + GridPane.setHgrow(messageLabel1, Priority.ALWAYS); + GridPane.setRowIndex(messageLabel1, ++rowIndex); + GridPane.setColumnIndex(messageLabel1, 0); + GridPane.setColumnSpan(messageLabel1, 2); + gridPane.getChildren().add(messageLabel1); + GridPane.setMargin(messageLabel1, new Insets(10, 0, 10, 0)); + AutoTooltipLabel messageLabel2 = new AutoTooltipLabel(Res.get("tradeFeedbackWindow.msg.part2")); messageLabel2.setMouseTransparent(true); messageLabel2.setWrapText(true); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java index 1af5d97fbf..417e5c0043 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxDetailsWindow.java @@ -30,7 +30,6 @@ import static haveno.desktop.util.FormBuilder.addConfirmationLabelLabel; import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; import static haveno.desktop.util.FormBuilder.addMultilineLabel; -import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import java.math.BigInteger; @@ -50,21 +49,20 @@ public class TxDetailsWindow extends Overlay { this.item = item; rowIndex = -1; width = 918; + if (headLine == null) + headLine = Res.get("txDetailsWindow.headline"); createGridPane(); gridPane.setHgap(15); addHeadLine(); addContent(); addButtons(); - addDontShowAgainCheckBox(); applyStyles(); display(); } protected void addContent() { - int rows = 10; MoneroTxWallet tx = item.getTx(); String memo = tx.getNote(); - if (memo != null && !"".equals(memo)) rows++; String txKey = null; boolean isOutgoing = tx.getOutgoingTransfer() != null; if (isOutgoing) { @@ -74,18 +72,11 @@ public class TxDetailsWindow extends Overlay { // TODO (monero-java): wallet.getTxKey() should return null if key does not exist instead of throwing exception } } - if (txKey != null && !"".equals(txKey)) rows++; - - // add title - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("txDetailsWindow.headline")); - Region spacer = new Region(); - spacer.setMinHeight(15); - gridPane.add(spacer, 0, ++rowIndex); // add sent or received note String resKey = isOutgoing ? "txDetailsWindow.xmr.noteSent" : "txDetailsWindow.xmr.noteReceived"; GridPane.setColumnSpan(addMultilineLabel(gridPane, ++rowIndex, Res.get(resKey), 0), 2); - spacer = new Region(); + Region spacer = new Region(); spacer.setMinHeight(15); gridPane.add(spacer, 0, ++rowIndex); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java index f3dfc654b8..f6f153eb8a 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TxWithdrawWindow.java @@ -52,7 +52,6 @@ public class TxWithdrawWindow extends Overlay { addHeadLine(); addContent(); addButtons(); - addDontShowAgainCheckBox(); applyStyles(); display(); } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java index fd60d07193..a8bd993219 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java @@ -77,6 +77,7 @@ public class VerifyDisputeResultSignatureWindow extends Overlay { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab; private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab; - private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase()); + private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed")); private Tab currentTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @@ -67,6 +67,7 @@ public class PortfolioView extends ActivatableView { private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen; private OpenOffer openOffer; private OpenOffersView openOffersView; + private boolean tabListChangeListenerAdded = false; @Inject public PortfolioView(CachingViewLoader viewLoader, Navigation navigation, FailedTradesManager failedTradesManager) { @@ -80,9 +81,9 @@ public class PortfolioView extends ActivatableView { root.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); failedTradesTab.setClosable(false); - openOffersTab.setText(Res.get("portfolio.tab.openOffers").toUpperCase()); - pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades").toUpperCase()); - closedTradesTab.setText(Res.get("portfolio.tab.history").toUpperCase()); + openOffersTab.setText(Res.get("portfolio.tab.openOffers")); + pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades")); + closedTradesTab.setText(Res.get("portfolio.tab.history")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(PortfolioView.class) == 1) @@ -168,7 +169,10 @@ public class PortfolioView extends ActivatableView { root.getTabs().add(failedTradesTab); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); - root.getTabs().addListener(tabListChangeListener); + if (!tabListChangeListenerAdded) { + root.getTabs().addListener(tabListChangeListener); + tabListChangeListenerAdded = true; // add listener only once + } navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == openOffersTab) @@ -194,7 +198,6 @@ public class PortfolioView extends ActivatableView { @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); - root.getTabs().removeListener(tabListChangeListener); navigation.removeListener(navigationListener); currentTab = null; } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 24d792005c..f1c53bcab1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -108,12 +108,12 @@ class CloneOfferDataModel extends MutableOfferDataModel { Offer offer = openOffer.getOffer(); direction = offer.getDirection(); - CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()) + CurrencyUtil.getTradeCurrency(offer.getCounterCurrencyCode()) .ifPresent(c -> this.tradeCurrency = c); - tradeCurrencyCode.set(offer.getCurrencyCode()); + tradeCurrencyCode.set(offer.getCounterCurrencyCode()); PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); - Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java index e48bdf80a7..4036d32d6b 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -110,7 +110,7 @@ public class CloneOfferView extends MutableOfferView { volumeCurrencyLabel.setDisable(true); // Workaround to fix margin on top of amount group - gridPane.setPadding(new Insets(-20, 25, -1, 25)); + gridPane.setPadding(new Insets(-20, 25, 25, 25)); updatePriceToggle(); updateElementsWithDirection(); @@ -144,7 +144,7 @@ public class CloneOfferView extends MutableOfferView { model.applyOpenOffer(openOffer); initWithData(openOffer.getOffer().getDirection(), - CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()).get(), false, null); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java index 7b6ca92a19..9185068d4e 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesListItem.java @@ -104,7 +104,7 @@ public class ClosedTradesListItem implements FilterableListItem { OfferDirection direction = closedTradableManager.wasMyOffer(offer) || tradable instanceof ArbitratorTrade ? offer.getDirection() : offer.getMirroredDirection(); - String currencyCode = tradable.getOffer().getCurrencyCode(); + String currencyCode = tradable.getOffer().getCounterCurrencyCode(); return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); } @@ -117,7 +117,7 @@ public class ClosedTradesListItem implements FilterableListItem { } public String getMarketLabel() { - return CurrencyUtil.getCurrencyPair(tradable.getOffer().getCurrencyCode()); + return CurrencyUtil.getCurrencyPair(tradable.getOffer().getCounterCurrencyCode()); } public String getState() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java index ab92f845db..aa7c6bb1c5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -156,6 +156,8 @@ public class ClosedTradesView extends ActivatableViewAndModel onWidthChange((double) newValue); tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString().replace(" BTC", ""))); buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString())); @@ -252,6 +254,7 @@ public class ClosedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -463,7 +465,7 @@ public class ClosedTradesView extends ActivatableViewAndModel setAvatarColumnCellFactory() { - avatarColumn.getStyleClass().addAll("last-column", "avatar-column"); + avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); avatarColumn.setCellFactory( new Callback<>() { @@ -696,7 +698,7 @@ public class ClosedTradesView extends ActivatableViewAndModel onRevertTrade(trade)); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java index 7cb24ca3eb..b449844b5b 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferView.java @@ -54,7 +54,7 @@ public class DuplicateOfferView extends MutableOfferView this.tradeCurrency = c); - tradeCurrencyCode.set(offer.getCurrencyCode()); + tradeCurrencyCode.set(offer.getCounterCurrencyCode()); this.initialState = openOffer.getState(); PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); - Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()); if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); @@ -130,9 +130,9 @@ class EditOfferDataModel extends MutableOfferDataModel { // by percentage than the restriction. We can't determine the percentage originally entered at offer // creation, so just use the default value as it doesn't matter anyway. double securityDepositPercent = CoinUtil.getAsPercentPerXmr(offer.getMaxSellerSecurityDeposit(), offer.getAmount()); - if (securityDepositPercent > Restrictions.getMaxSecurityDepositAsPercent() + if (securityDepositPercent > Restrictions.getMaxSecurityDepositPct() && offer.getMaxSellerSecurityDeposit().equals(Restrictions.getMinSecurityDeposit())) - securityDepositPct.set(Restrictions.getDefaultSecurityDepositAsPercent()); + securityDepositPct.set(Restrictions.getDefaultSecurityDepositPct()); else securityDepositPct.set(securityDepositPercent); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index 8b1d9775e6..90eb14b942 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -103,7 +103,7 @@ public class EditOfferView extends MutableOfferView { volumeCurrencyLabel.setDisable(true); // Workaround to fix margin on top of amount group - gridPane.setPadding(new Insets(-20, 25, -1, 25)); + gridPane.setPadding(new Insets(-20, 25, 25, 25)); updatePriceToggle(); updateElementsWithDirection(); @@ -141,7 +141,7 @@ public class EditOfferView extends MutableOfferView { model.applyOpenOffer(openOffer); initWithData(openOffer.getOffer().getDirection(), - CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCounterCurrencyCode()).get(), false, null); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java index dbbfa956ba..b64abf022f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java @@ -68,7 +68,11 @@ class FailedTradesDataModel extends ActivatableDataModel { private void applyList() { list.clear(); - list.addAll(failedTradesManager.getObservableList().stream().map(FailedTradesListItem::new).collect(Collectors.toList())); + list.addAll( + failedTradesManager.getObservableList().stream() + .map(trade -> new FailedTradesListItem(trade, failedTradesManager)) + .collect(Collectors.toList()) + ); // we sort by date, earliest first list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesListItem.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesListItem.java index 0b7b7c3a0a..47e80efcec 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesListItem.java @@ -17,18 +17,117 @@ package haveno.desktop.main.portfolio.failedtrades; +import org.apache.commons.lang3.StringUtils; + +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.Res; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.trade.ArbitratorTrade; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; +import haveno.core.trade.failed.FailedTradesManager; +import haveno.core.util.FormattingUtils; +import haveno.core.util.VolumeUtil; +import haveno.desktop.util.DisplayUtils; +import haveno.desktop.util.filtering.FilterableListItem; +import haveno.desktop.util.filtering.FilteringUtils; import lombok.Getter; -class FailedTradesListItem { +class FailedTradesListItem implements FilterableListItem { @Getter private final Trade trade; + private final FailedTradesManager failedTradesManager; - FailedTradesListItem(Trade trade) { + FailedTradesListItem(Trade trade, + FailedTradesManager failedTradesManager) { this.trade = trade; + this.failedTradesManager = failedTradesManager; } FailedTradesListItem() { this.trade = null; + this.failedTradesManager = null; + } + + public String getDateAsString() { + return DisplayUtils.formatDateTime(trade.getDate()); + } + + public String getPriceAsString() { + return FormattingUtils.formatPrice(trade.getPrice()); + } + + public String getAmountAsString() { + return HavenoUtils.formatXmr(trade.getAmount()); + } + + public String getPaymentMethod() { + return trade.getOffer().getPaymentMethodNameWithCountryCode(); + } + + public String getMarketDescription() { + return CurrencyUtil.getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); + } + + public String getDirectionLabel() { + Offer offer = trade.getOffer(); + OfferDirection direction = failedTradesManager.wasMyOffer(offer) || trade instanceof ArbitratorTrade + ? offer.getDirection() + : offer.getMirroredDirection(); + String currencyCode = trade.getOffer().getCounterCurrencyCode(); + return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); + } + + public String getVolumeAsString() { + return VolumeUtil.formatVolumeWithCode(trade.getVolume()); + } + + public String getState() { + return Res.get("portfolio.failed.Failed"); + } + + @Override + public boolean match(String filterString) { + if (filterString.isEmpty()) { + return true; + } + if (StringUtils.containsIgnoreCase(getDateAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getMarketDescription(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getPriceAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getPaymentMethod(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getAmountAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getDirectionLabel(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getVolumeAsString(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getState(), filterString)) { + return true; + } + if (StringUtils.containsIgnoreCase(getTrade().getOffer().getCombinedExtraInfo(), filterString)) { + return true; + } + if (trade.getBuyer().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString)) { + return true; + } + if (trade.getSeller().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString)) { + return true; + } + if (FilteringUtils.match(trade.getOffer(), filterString)) { + return true; + } + return FilteringUtils.match(trade, filterString); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.fxml index 517b4b997a..6802352657 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.fxml @@ -17,6 +17,7 @@ ~ along with Haveno. If not, see . --> + @@ -33,11 +34,8 @@ - - - - - + + diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java index 35fe31dc22..26af551f5f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -23,8 +23,6 @@ import com.jfoenix.controls.JFXButton; import de.jensd.fx.fontawesome.AwesomeIcon; import haveno.common.util.Utilities; import haveno.core.locale.Res; -import haveno.core.offer.Offer; -import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.user.User; @@ -34,7 +32,7 @@ import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.HyperlinkWithIcon; -import haveno.desktop.components.InputTextField; +import haveno.desktop.components.list.FilterBox; import haveno.desktop.main.offer.OfferViewUtil; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; @@ -43,7 +41,6 @@ import haveno.desktop.util.GUIUtil; import java.math.BigInteger; import java.util.Comparator; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -62,7 +59,6 @@ import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -78,13 +74,7 @@ public class FailedTradesView extends ActivatableViewAndModel priceColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, removeTradeColumn; @FXML - HBox searchBox; - @FXML - AutoTooltipLabel filterLabel; - @FXML - InputTextField filterTextField; - @FXML - Pane searchBoxSpacer; + FilterBox filterBox; @FXML Label numItems; @FXML @@ -96,7 +86,6 @@ public class FailedTradesView extends ActivatableViewAndModel sortedList; private FilteredList filteredList; private EventHandler keyEventEventHandler; - private ChangeListener filterTextFieldListener; private Scene scene; private XmrWalletService xmrWalletService; private User user; @@ -115,6 +104,8 @@ public class FailedTradesView extends ActivatableViewAndModel o.getTrade().getPrice())); volumeColumn.setComparator(Comparator.comparing(o -> o.getTrade().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); amountColumn.setComparator(Comparator.comparing(o -> o.getTrade().getAmount(), Comparator.nullsFirst(Comparator.naturalOrder()))); - stateColumn.setComparator(Comparator.comparing(model::getState)); - marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); + stateColumn.setComparator(Comparator.comparing(o -> o.getState())); + marketColumn.setComparator(Comparator.comparing(o -> o.getMarketDescription())); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); @@ -168,12 +159,6 @@ public class FailedTradesView extends ActivatableViewAndModel applyFilteredListPredicate(filterTextField.getText()); - searchBox.setSpacing(5); - HBox.setHgrow(searchBoxSpacer, Priority.ALWAYS); - numItems.setId("num-offers"); numItems.setPadding(new Insets(-5, 0, 0, 10)); HBox.setHgrow(footerSpacer, Priority.ALWAYS); @@ -193,6 +178,10 @@ public class FailedTradesView extends ActivatableViewAndModel { - ObservableList> tableColumns = tableView.getColumns(); + ObservableList> tableColumns = GUIUtil.getContentColumns(tableView); int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; @@ -242,14 +231,14 @@ public class FailedTradesView extends ActivatableViewAndModel contentConverter = item -> { String[] columns = new String[reportColumns]; - columns[0] = model.getTradeId(item); - columns[1] = model.getDate(item); - columns[2] = model.getMarketLabel(item); - columns[3] = model.getPrice(item); - columns[4] = model.getAmount(item); - columns[5] = model.getVolume(item); - columns[6] = model.getDirectionLabel(item); - columns[7] = model.getState(item); + columns[0] = item.getTrade().getId(); + columns[1] = item.getDateAsString(); + columns[2] = item.getMarketDescription(); + columns[3] = item.getPriceAsString(); + columns[4] = item.getAmountAsString(); + columns[5] = item.getVolumeAsString(); + columns[6] = item.getDirectionLabel(); + columns[7] = item.getState(); return columns; }; @@ -260,9 +249,6 @@ public class FailedTradesView extends ActivatableViewAndModel { - if (filterString.isEmpty()) - return true; - - Offer offer = item.getTrade().getOffer(); - - if (offer.getId().contains(filterString)) { - return true; - } - if (model.getDate(item).contains(filterString)) { - return true; - } - if (model.getMarketLabel(item).contains(filterString)) { - return true; - } - if (model.getPrice(item).contains(filterString)) { - return true; - } - if (model.getVolume(item).contains(filterString)) { - return true; - } - if (model.getAmount(item).contains(filterString)) { - return true; - } - if (model.getDirectionLabel(item).contains(filterString)) { - return true; - } - - Trade trade = item.getTrade(); - - if (trade.getMaker().getDepositTxHash() != null && trade.getMaker().getDepositTxHash().contains(filterString)) { - return true; - } - if (trade.getTaker().getDepositTxHash() != null && trade.getTaker().getDepositTxHash().contains(filterString)) { - return true; - } - if (trade.getPayoutTxId() != null && trade.getPayoutTxId().contains(filterString)) { - return true; - } - - Contract contract = trade.getContract(); - - boolean isBuyerOnion = false; - boolean isSellerOnion = false; - boolean matchesBuyersPaymentAccountData = false; - boolean matchesSellersPaymentAccountData = false; - if (contract != null) { - isBuyerOnion = contract.getBuyerNodeAddress().getFullAddress().contains(filterString); - isSellerOnion = contract.getSellerNodeAddress().getFullAddress().contains(filterString); - matchesBuyersPaymentAccountData = trade.getBuyer().getPaymentAccountPayload().getPaymentDetails().contains(filterString); - matchesSellersPaymentAccountData = trade.getSeller().getPaymentAccountPayload().getPaymentDetails().contains(filterString); - } - return isBuyerOnion || isSellerOnion || - matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; - }); - } private void onUnfail() { Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); @@ -385,7 +314,6 @@ public class FailedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -400,7 +328,7 @@ public class FailedTradesView extends ActivatableViewAndModel tradeDetailsWindow.show(item.getTrade())); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); @@ -427,7 +355,7 @@ public class FailedTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(trade.getValue())); stateColumn.setCellFactory( new Callback<>() { @@ -467,7 +397,7 @@ public class FailedTradesView extends ActivatableViewAndModel implements ViewModel { @@ -44,45 +39,4 @@ class FailedTradesViewModel extends ActivatableWithDataModel getList() { return dataModel.getList(); } - - String getTradeId(FailedTradesListItem item) { - return item.getTrade().getShortId(); - } - - String getAmount(FailedTradesListItem item) { - if (item != null && item.getTrade() != null) - return HavenoUtils.formatXmr(item.getTrade().getAmount()); - else - return ""; - } - - String getPrice(FailedTradesListItem item) { - return (item != null) ? FormattingUtils.formatPrice(item.getTrade().getPrice()) : ""; - } - - String getVolume(FailedTradesListItem item) { - if (item != null && item.getTrade() != null) - return VolumeUtil.formatVolumeWithCode(item.getTrade().getVolume()); - else - return ""; - } - - String getDirectionLabel(FailedTradesListItem item) { - return (item != null) ? DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getTrade().getOffer()), item.getTrade().getOffer().getCurrencyCode(), item.getTrade().getOffer().isPrivateOffer()) : ""; - } - - String getMarketLabel(FailedTradesListItem item) { - if ((item == null)) - return ""; - - return CurrencyUtil.getCurrencyPair(item.getTrade().getOffer().getCurrencyCode()); - } - - String getDate(FailedTradesListItem item) { - return DisplayUtils.formatDateTime(item.getTrade().getDate()); - } - - String getState(FailedTradesListItem item) { - return item != null ? Res.get("portfolio.failed.Failed") : ""; - } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java index 5a178f6dce..34f205f4a2 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersDataModel.java @@ -95,6 +95,6 @@ class OpenOffersDataModel extends ActivatableDataModel { } boolean isTriggered(OpenOffer openOffer) { - return TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer); + return TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCounterCurrencyCode()), openOffer); } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 035ec5fbdc..44b26f1bd3 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -35,7 +35,6 @@ - diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 3282ab078a..1072cfa9f7 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -116,8 +116,6 @@ public class OpenOffersView extends ActivatableViewAndModel onWidthChange((double) newValue); groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString())); paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); @@ -231,8 +231,7 @@ public class OpenOffersView extends ActivatableViewAndModel applyFilteredListPredicate(filterTextField.getText()); searchBox.setSpacing(5); HBox.setHgrow(searchBoxSpacer, Priority.ALWAYS); @@ -470,8 +469,8 @@ public class OpenOffersView extends ActivatableViewAndModel navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); @@ -527,7 +526,7 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); - offerIdColumn.getStyleClass().addAll("number-column", "first-column"); + offerIdColumn.getStyleClass().addAll("number-column"); offerIdColumn.setCellFactory( new Callback<>() { @@ -903,7 +902,7 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); removeItemColumn.setCellFactory( new Callback<>() { @@ -1011,35 +1010,31 @@ public class OpenOffersView extends ActivatableViewAndModel call(TableColumn column) { return new TableCell<>() { - Button button; + private final Button button = getRegularIconButton(MaterialDesignIcon.SHIELD_HALF_FULL); @Override - public void updateItem(final OpenOfferListItem item, boolean empty) { + protected void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - if (button == null) { - button = getRegularIconButton(MaterialDesignIcon.SHIELD_HALF_FULL); - boolean triggerPriceSet = item.getOpenOffer().getTriggerPrice() > 0; - button.setVisible(triggerPriceSet); - - if (model.dataModel.isTriggered(item.getOpenOffer())) { - button.getGraphic().getStyleClass().add("warning"); - button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); - } else { - button.getGraphic().getStyleClass().remove("warning"); - button.setTooltip(new Tooltip(Res.get("openOffer.triggerPrice", model.getTriggerPrice(item)))); - } - setGraphic(button); - } - button.setOnAction(event -> onEditOpenOffer(item.getOpenOffer())); - } else { + if (item == null || empty) { setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } + button.setOnAction(null); + return; } + + boolean triggerPriceSet = item.getOpenOffer().getTriggerPrice() > 0; + button.setVisible(triggerPriceSet); + + if (model.dataModel.isTriggered(item.getOpenOffer())) { + button.getGraphic().getStyleClass().add("warning"); + button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); + } else { + button.getGraphic().getStyleClass().remove("warning"); + button.setTooltip(new Tooltip(Res.get("openOffer.triggerPrice", model.getTriggerPrice(item)))); + } + + button.setOnAction(e -> onEditOpenOffer(item.getOpenOffer())); + setGraphic(button); } }; } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 616537f5ec..42b2f7be1e 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -118,21 +118,21 @@ class OpenOffersViewModel extends ActivatableWithDataModel } String getVolume(OpenOfferListItem item) { - return (item != null) ? VolumeUtil.formatVolume(item.getOffer(), false, 0) + " " + item.getOffer().getCurrencyCode() : ""; + return (item != null) ? VolumeUtil.formatVolume(item.getOffer(), false, 0) + " " + item.getOffer().getCounterCurrencyCode() : ""; } String getDirectionLabel(OpenOfferListItem item) { if ((item == null)) return ""; - return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode(), item.getOffer().isPrivateOffer()); + return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCounterCurrencyCode(), item.getOffer().isPrivateOffer()); } String getMarketLabel(OpenOfferListItem item) { if ((item == null)) return ""; - return CurrencyUtil.getCurrencyPair(item.getOffer().getCurrencyCode()); + return CurrencyUtil.getCurrencyPair(item.getOffer().getCounterCurrencyCode()); } String getPaymentMethod(OpenOfferListItem item) { @@ -168,7 +168,7 @@ class OpenOffersViewModel extends ActivatableWithDataModel if (!offer.isUseMarketBasedPrice() || triggerPrice <= 0) { return Res.get("shared.na"); } else { - return PriceUtil.formatMarketPrice(triggerPrice, offer.getCurrencyCode()); + return PriceUtil.formatMarketPrice(triggerPrice, offer.getCounterCurrencyCode()); } } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java index 3b9a399e82..0ad44bee1f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesListItem.java @@ -17,6 +17,7 @@ package haveno.desktop.main.portfolio.pendingtrades; +import haveno.core.locale.CurrencyUtil; import haveno.core.monetary.Price; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; @@ -27,8 +28,6 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static haveno.core.locale.CurrencyUtil.getCurrencyPair; - /** * We could remove that wrapper if it is not needed for additional UI only fields. */ @@ -63,7 +62,7 @@ public class PendingTradesListItem implements FilterableListItem { } public String getMarketDescription() { - return getCurrencyPair(trade.getOffer().getCurrencyCode()); + return CurrencyUtil.getCurrencyPair(trade.getOffer().getCounterCurrencyCode()); } @Override diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml index c0240455f2..a9e35c48f1 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml @@ -25,7 +25,7 @@ + spacing="10" xmlns:fx="http://javafx.com/fxml"> diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 645fbb5014..054f3cd853 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -55,6 +55,7 @@ import haveno.desktop.main.shared.ChatView; import haveno.desktop.util.CssTheme; import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.FormBuilder; +import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import java.util.Comparator; import java.util.HashMap; @@ -171,6 +172,8 @@ public class PendingTradesView extends ActivatableViewAndModel) change -> { + tableView.setMinHeight(getMinTableViewHeight()); + }); + selectedItemSubscription = EasyBind.subscribe(model.dataModel.selectedItemProperty, selectedItem -> { if (selectedItem != null) { if (selectedSubView != null) @@ -338,6 +347,10 @@ public class PendingTradesView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(pendingTradesListItem.getValue())); tradeIdColumn.setCellFactory( new Callback<>() { @@ -821,7 +833,7 @@ public class PendingTradesView extends ActivatableViewAndModel setAvatarColumnCellFactory() { avatarColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); - avatarColumn.getStyleClass().addAll("last-column", "avatar-column"); + avatarColumn.getStyleClass().add("avatar-column"); avatarColumn.setCellFactory( new Callback<>() { @@ -860,7 +872,7 @@ public class PendingTradesView extends ActivatableViewAndModel setChatColumnCellFactory() { chatColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); - chatColumn.getStyleClass().addAll("last-column", "avatar-column"); + chatColumn.getStyleClass().addAll("avatar-column"); chatColumn.setSortable(false); chatColumn.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 6a34504dad..8bbc7a7371 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -27,11 +27,8 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; -import haveno.core.trade.ArbitratorTrade; -import haveno.core.trade.BuyerTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; -import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeUtil; import haveno.core.user.User; @@ -48,6 +45,8 @@ import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; @@ -107,6 +106,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel showPaymentDetailsEarly = new HashMap(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -266,7 +266,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel firstHalfOverWarnTextSupplier = () -> ""; private Supplier periodOverWarnTextSupplier = () -> ""; + private Supplier depositTxMissingWarnTextSupplier = () -> ""; TradeStepInfo(TitledGroupBg titledGroupBg, SimpleMarkdownLabel label, @@ -95,6 +97,10 @@ public class TradeStepInfo { this.periodOverWarnTextSupplier = periodOverWarnTextSupplier; } + public void setDepositTxMissingWarnTextSupplier(Supplier depositTxMissingWarnTextSupplier) { + this.depositTxMissingWarnTextSupplier = depositTxMissingWarnTextSupplier; + } + public void setState(State state) { this.state = state; switch (state) { @@ -192,12 +198,23 @@ public class TradeStepInfo { button.getStyleClass().remove("action-button"); button.setDisable(false); break; + case DEPOSIT_MISSING: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.support.headline.depositTxMissing")); + label.updateContent(depositTxMissingWarnTextSupplier.get()); + button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(false); + break; case TRADE_COMPLETED: // hide group titledGroupBg.setVisible(false); label.setVisible(false); button.setVisible(false); footerLabel.setVisible(false); + default: + break; } if (trade != null && trade.getPayoutTxId() != null) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 3af019e6d0..a916a4c3a9 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -34,7 +34,6 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; -import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.desktop.components.InfoTextField; import haveno.desktop.components.TitledGroupBg; @@ -50,8 +49,6 @@ import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelTxIdTextField; import haveno.desktop.util.Layout; import haveno.network.p2p.BootstrapListener; -import java.time.Duration; -import java.time.Instant; import java.util.Optional; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; @@ -80,7 +77,7 @@ public abstract class TradeStepView extends AnchorPane { protected final Preferences preferences; protected final GridPane gridPane; - private Subscription tradePeriodStateSubscription, disputeStateSubscription, mediationResultStateSubscription; + private Subscription tradePeriodStateSubscription, tradeStateSubscription, disputeStateSubscription, mediationResultStateSubscription; protected int gridRow = 0; private TextField timeLeftTextField; private ProgressBar timeLeftProgressBar; @@ -125,6 +122,7 @@ public abstract class TradeStepView extends AnchorPane { gridPane.setHgap(Layout.GRID_GAP); gridPane.setVgap(Layout.GRID_GAP); + gridPane.setPadding(new Insets(0, 0, 25, 0)); ColumnConstraints columnConstraints1 = new ColumnConstraints(); columnConstraints1.setHgrow(Priority.ALWAYS); @@ -143,8 +141,10 @@ public abstract class TradeStepView extends AnchorPane { addContent(); errorMessageListener = (observable, oldValue, newValue) -> { - if (newValue != null) + if (newValue != null) { + log.warn("Showing popup for trade error {} {}", trade.getClass().getSimpleName(), trade.getId(), new RuntimeException(newValue)); new Popup().error(newValue).show(); + } }; clockListener = new ClockWatcher.Listener() { @@ -191,7 +191,7 @@ public abstract class TradeStepView extends AnchorPane { trade.errorMessageProperty().addListener(errorMessageListener); tradeStepInfo.setOnAction(e -> { - if (!isArbitrationOpenedState() && this.isTradePeriodOver()) { + if (!isArbitrationOpenedState() && (this.isTradePeriodOver() || trade.isDepositTxMissing())) { openSupportTicket(); } else { openChat(); @@ -225,7 +225,7 @@ public abstract class TradeStepView extends AnchorPane { infoLabel.setText(getInfoText()); } - BooleanProperty initialized = model.dataModel.tradeManager.getPersistedTradesInitialized(); + BooleanProperty initialized = model.dataModel.tradeManager.getTradesInitialized(); if (initialized.get()) { onPendingTradesInitialized(); } else { @@ -257,15 +257,28 @@ public abstract class TradeStepView extends AnchorPane { } }); + if (trade.wasWalletPolled.get()) addTradeStateSubscription(); + else trade.wasWalletPolled.addListener((observable, oldValue, newValue) -> { + if (newValue) addTradeStateSubscription(); + }); + UserThread.execute(() -> model.p2PService.removeP2PServiceListener(bootstrapListener)); } + private void addTradeStateSubscription() { + tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { + if (newValue != null) { + UserThread.execute(() -> updateTradeState(newValue)); + } + }); + } + private void openSupportTicket() { - if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { - new Popup().warning(Res.get("portfolio.pending.error.depositTxNotConfirmed")).show(); - } else { + if (trade.isDepositTxMissing() || trade.getPhase().ordinal() >= Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) { applyOnDisputeOpened(); model.dataModel.onOpenDispute(); + } else { + new Popup().warning(Res.get("portfolio.pending.error.depositTxNotConfirmed")).show(); } } @@ -298,6 +311,8 @@ public abstract class TradeStepView extends AnchorPane { if (tradePeriodStateSubscription != null) tradePeriodStateSubscription.unsubscribe(); + if (tradeStateSubscription != null) + tradeStateSubscription.unsubscribe(); if (clockListener != null) model.clockWatcher.removeListener(clockListener); @@ -446,6 +461,7 @@ public abstract class TradeStepView extends AnchorPane { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); tradeStepInfo.setPeriodOverWarnTextSupplier(this::getPeriodOverWarnText); + tradeStepInfo.setDepositTxMissingWarnTextSupplier(this::getDepositTxMissingWarnText); } protected void hideTradeStepInfo() { @@ -465,6 +481,10 @@ public abstract class TradeStepView extends AnchorPane { return ""; } + protected String getDepositTxMissingWarnText() { + return Res.get("portfolio.pending.support.depositTxMissing"); + } + protected void applyOnDisputeOpened() { } @@ -742,7 +762,7 @@ public abstract class TradeStepView extends AnchorPane { } protected String getCurrencyCode(Trade trade) { - return checkNotNull(trade.getOffer()).getCurrencyCode(); + return checkNotNull(trade.getOffer()).getCounterCurrencyCode(); } protected boolean isXmrTrade() { @@ -781,34 +801,40 @@ public abstract class TradeStepView extends AnchorPane { } } -// private void checkIfLockTimeIsOver() { -// if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) { -// Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); -// if (delayedPayoutTx != null) { -// long lockTime = delayedPayoutTx.getLockTime(); -// int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight(); -// long remaining = lockTime - bestChainHeight; -// if (remaining <= 0) { -// openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); -// } -// } -// } -// } - - protected void checkForUnconfirmedTimeout() { - if (trade.isDepositsConfirmed()) return; - long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours(); - if (unconfirmedHours >= 3 && !trade.hasFailed()) { - String key = "tradeUnconfirmedTooLong_" + trade.getShortId(); - if (DontShowAgainLookup.showAgain(key)) { - new Popup().warning(Res.get("portfolio.pending.unconfirmedTooLong", trade.getShortId(), unconfirmedHours)) - .dontShowAgainId(key) - .closeButtonText(Res.get("shared.ok")) - .show(); - } + private void updateTradeState(Trade.State tradeState) { + if (!trade.getDisputeState().isOpen() && trade.isDepositTxMissing()) { + tradeStepInfo.setState(TradeStepInfo.State.DEPOSIT_MISSING); } } + // private void checkIfLockTimeIsOver() { + // if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) { + // Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + // if (delayedPayoutTx != null) { + // long lockTime = delayedPayoutTx.getLockTime(); + // int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight(); + // long remaining = lockTime - bestChainHeight; + // if (remaining <= 0) { + // openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); + // } + // } + // } + // } + + // protected void checkForUnconfirmedTimeout() { + // if (trade.isDepositsConfirmed()) return; + // long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours(); + // if (unconfirmedHours >= 3 && !trade.hasFailed()) { + // String key = "tradeUnconfirmedTooLong_" + trade.getShortId(); + // if (DontShowAgainLookup.showAgain(key)) { + // new Popup().warning(Res.get("portfolio.pending.unconfirmedTooLong", trade.getShortId(), unconfirmedHours)) + // .dontShowAgainId(key) + // .closeButtonText(Res.get("shared.ok")) + // .show(); + // } + // } + // } + /////////////////////////////////////////////////////////////////////////////////////////// // TradeDurationLimitInfo /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index 4888d5b4a8..e1936d5cbc 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -36,7 +36,7 @@ public class BuyerStep1View extends TradeStepView { super.onPendingTradesInitialized(); //validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else? //validateDepositInputs(); - checkForUnconfirmedTimeout(); + //checkForUnconfirmedTimeout(); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 51a36488eb..f02c575b70 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -108,9 +108,12 @@ import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextArea; +import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.text.Font; + import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -129,6 +132,9 @@ public class BuyerStep2View extends TradeStepView { private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; private Timer timeoutTimer; + private int paymentAccountGridRow = 0; + private GridPane paymentAccountGridPane; + private GridPane moreConfirmationsGridPane; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation @@ -224,202 +230,216 @@ public class BuyerStep2View extends TradeStepView { gridPane.getColumnConstraints().get(1).setHgrow(Priority.ALWAYS); addTradeInfoBlock(); + createPaymentDetailsGridPane(); + createRecommendationGridPane(); + // attach grid pane based on current state + EasyBind.subscribe(trade.statePhaseProperty(), newValue -> { + if (trade.isPaymentSent() || model.getShowPaymentDetailsEarly() || trade.isDepositsFinalized()) { + attachPaymentDetailsGrid(); + } else { + attachRecommendationGrid(); + } + }); + } + private void createPaymentDetailsGridPane() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : ""; - TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4, + + paymentAccountGridPane = createGridPane(); + TitledGroupBg accountTitledGroupBg = addTitledGroupBg(paymentAccountGridPane, paymentAccountGridRow, 4, Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)), Layout.COMPACT_GROUP_DISTANCE); - TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 0, + TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.amountToTransfer"), model.getFiatVolume(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE).second; field.setCopyWithoutCurrencyPostFix(true); - //preland: this fixes a textarea layout glitch + //preland: this fixes a textarea layout glitch // TODO: can this be removed now? TextArea uiHack = new TextArea(); uiHack.setMaxHeight(1); GridPane.setRowIndex(uiHack, 1); GridPane.setMargin(uiHack, new Insets(0, 0, 0, 0)); uiHack.setVisible(false); - gridPane.getChildren().add(uiHack); + paymentAccountGridPane.getChildren().add(uiHack); switch (paymentMethodId) { case PaymentMethod.UPHOLD_ID: - gridRow = UpholdForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = UpholdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONEY_BEAM_ID: - gridRow = MoneyBeamForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = MoneyBeamForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.POPMONEY_ID: - gridRow = PopmoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PopmoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.REVOLUT_ID: - gridRow = RevolutForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = RevolutForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PERFECT_MONEY_ID: - gridRow = PerfectMoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PerfectMoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SEPA_ID: - gridRow = SepaForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SepaForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SEPA_INSTANT_ID: - gridRow = SepaInstantForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SepaInstantForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.FASTER_PAYMENTS_ID: - gridRow = FasterPaymentsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = FasterPaymentsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NATIONAL_BANK_ID: - gridRow = NationalBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = NationalBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.AUSTRALIA_PAYID_ID: - gridRow = AustraliaPayidForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = AustraliaPayidForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SAME_BANK_ID: - gridRow = SameBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SameBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SPECIFIC_BANKS_ID: - gridRow = SpecificBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SpecificBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SWISH_ID: - gridRow = SwishForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SwishForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ALI_PAY_ID: - gridRow = AliPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = AliPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.WECHAT_PAY_ID: - gridRow = WeChatPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = WeChatPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ZELLE_ID: - gridRow = ZelleForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = ZelleForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CHASE_QUICK_PAY_ID: - gridRow = ChaseQuickPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = ChaseQuickPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.INTERAC_E_TRANSFER_ID: - gridRow = InteracETransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = InteracETransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.JAPAN_BANK_ID: - gridRow = JapanBankTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = JapanBankTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.US_POSTAL_MONEY_ORDER_ID: - gridRow = USPostalMoneyOrderForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = USPostalMoneyOrderForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_DEPOSIT_ID: - gridRow = CashDepositForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = CashDepositForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAY_BY_MAIL_ID: - gridRow = PayByMailForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PayByMailForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_AT_ATM_ID: - gridRow = CashAtAtmForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = CashAtAtmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONEY_GRAM_ID: - gridRow = MoneyGramForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = MoneyGramForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.WESTERN_UNION_ID: - gridRow = WesternUnionForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = WesternUnionForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.HAL_CASH_ID: - gridRow = HalCashForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = HalCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.F2F_ID: checkNotNull(model.dataModel.getTrade(), "model.dataModel.getTrade() must not be null"); checkNotNull(model.dataModel.getTrade().getOffer(), "model.dataModel.getTrade().getOffer() must not be null"); - gridRow = F2FForm.addStep2Form(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true); + paymentAccountGridRow = F2FForm.addStep2Form(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true); break; case PaymentMethod.BLOCK_CHAINS_ID: case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: String labelTitle = Res.get("portfolio.pending.step2_buyer.sellersAddress", getCurrencyName(trade)); - gridRow = AssetsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, labelTitle); + paymentAccountGridRow = AssetsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, labelTitle); break; case PaymentMethod.PROMPT_PAY_ID: - gridRow = PromptPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PromptPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.ADVANCED_CASH_ID: - gridRow = AdvancedCashForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = AdvancedCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TRANSFERWISE_ID: - gridRow = TransferwiseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = TransferwiseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TRANSFERWISE_USD_ID: - gridRow = TransferwiseUsdForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = TransferwiseUsdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYSERA_ID: - gridRow = PayseraForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PayseraForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAXUM_ID: - gridRow = PaxumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PaxumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NEFT_ID: - gridRow = NeftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = NeftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.RTGS_ID: - gridRow = RtgsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = RtgsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.IMPS_ID: - gridRow = ImpsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = ImpsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.UPI_ID: - gridRow = UpiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = UpiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYTM_ID: - gridRow = PaytmForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PaytmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.NEQUI_ID: - gridRow = NequiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = NequiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.BIZUM_ID: - gridRow = BizumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = BizumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PIX_ID: - gridRow = PixForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PixForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.AMAZON_GIFT_CARD_ID: - gridRow = AmazonGiftCardForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = AmazonGiftCardForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CAPITUAL_ID: - gridRow = CapitualForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = CapitualForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CELPAY_ID: - gridRow = CelPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = CelPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.MONESE_ID: - gridRow = MoneseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = MoneseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SATISPAY_ID: - gridRow = SatispayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = SatispayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.TIKKIE_ID: - gridRow = TikkieForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = TikkieForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.VERSE_ID: - gridRow = VerseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = VerseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.STRIKE_ID: - gridRow = StrikeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = StrikeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.SWIFT_ID: - gridRow = SwiftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, trade); + paymentAccountGridRow = SwiftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, trade); break; case PaymentMethod.ACH_TRANSFER_ID: - gridRow = AchTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = AchTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID: - gridRow = DomesticWireTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = DomesticWireTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.CASH_APP_ID: - gridRow = CashAppForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = CashAppForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYPAL_ID: - gridRow = PayPalForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PayPalForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.VENMO_ID: - gridRow = VenmoForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = VenmoForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; case PaymentMethod.PAYSAFE_ID: - gridRow = PaysafeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + paymentAccountGridRow = PaysafeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload); break; default: log.error("Not supported PaymentMethod: " + paymentMethodId); @@ -438,19 +458,19 @@ public class BuyerStep2View extends TradeStepView { .findFirst() .ifPresent(paymentAccount -> { String accountName = paymentAccount.getAccountName(); - addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, + addCompactTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, ++paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.buyerAccount"), accountName); }); } } - GridPane.setRowSpan(accountTitledGroupBg, gridRow - 1); + GridPane.setRowSpan(accountTitledGroupBg, gridRow + paymentAccountGridRow - 1); - Tuple4 tuple3 = addButtonBusyAnimationLabel(gridPane, ++gridRow, 0, + Tuple4 tuple3 = addButtonBusyAnimationLabel(paymentAccountGridPane, ++paymentAccountGridRow, 0, Res.get("portfolio.pending.step2_buyer.paymentSent"), 10); - HBox hBox = tuple3.fourth; - GridPane.setColumnSpan(hBox, 2); + HBox confirmButtonHBox = tuple3.fourth; + GridPane.setColumnSpan(confirmButtonHBox, 2); confirmButton = tuple3.first; confirmButton.setDisable(!confirmPaymentSentPermitted()); confirmButton.setOnAction(e -> onPaymentSent()); @@ -458,6 +478,64 @@ public class BuyerStep2View extends TradeStepView { statusLabel = tuple3.third; } + private void createRecommendationGridPane() { + + // create grid pane to show recommendation for more blocks + moreConfirmationsGridPane = new GridPane(); + moreConfirmationsGridPane.setStyle("-fx-background-color: -bs-content-background-gray;"); + moreConfirmationsGridPane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + + // add title + addTitledGroupBg(moreConfirmationsGridPane, 0, 1, Res.get("portfolio.pending.step1.waitForConf"), Layout.COMPACT_GROUP_DISTANCE); + + // add text + Label label = new Label(Res.get("portfolio.pending.step2_buyer.additionalConf", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED)); + label.setFont(new Font(16)); + GridPane.setMargin(label, new Insets(20, 0, 0, 0)); + moreConfirmationsGridPane.add(label, 0, 1, 2, 1); + + // add button to show payment details + Button showPaymentDetailsButton = new Button("Show payment details early"); + showPaymentDetailsButton.getStyleClass().add("action-button"); + GridPane.setMargin(showPaymentDetailsButton, new Insets(20, 0, 0, 0)); + showPaymentDetailsButton.setOnAction(e -> { + model.setShowPaymentDetailsEarly(true); + gridPane.getChildren().remove(moreConfirmationsGridPane); + gridPane.getChildren().add(paymentAccountGridPane); + GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1); + GridPane.setColumnSpan(paymentAccountGridPane, 2); + }); + moreConfirmationsGridPane.add(showPaymentDetailsButton, 0, 2); + } + + private GridPane createGridPane() { + GridPane gridPane = new GridPane(); + gridPane.setHgap(Layout.GRID_GAP); + gridPane.setVgap(Layout.GRID_GAP); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHgrow(Priority.ALWAYS); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + return gridPane; + } + + private void attachRecommendationGrid() { + if (gridPane.getChildren().contains(moreConfirmationsGridPane)) return; + if (gridPane.getChildren().contains(paymentAccountGridPane)) gridPane.getChildren().remove(paymentAccountGridPane); + gridPane.getChildren().add(moreConfirmationsGridPane); + GridPane.setRowIndex(moreConfirmationsGridPane, gridRow + 1); + GridPane.setColumnSpan(moreConfirmationsGridPane, 2); + } + + private void attachPaymentDetailsGrid() { + if (gridPane.getChildren().contains(paymentAccountGridPane)) return; + if (gridPane.getChildren().contains(moreConfirmationsGridPane)) gridPane.getChildren().remove(moreConfirmationsGridPane); + gridPane.getChildren().add(paymentAccountGridPane); + GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1); + GridPane.setColumnSpan(paymentAccountGridPane, 2); + } + private boolean confirmPaymentSentPermitted() { if (!trade.confirmPermitted()) return false; if (trade.getState() == Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) return true; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java index b28eda4a10..81605cf33f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java @@ -26,6 +26,7 @@ import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import haveno.desktop.util.Layout; import javafx.beans.value.ChangeListener; import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; @@ -72,6 +73,7 @@ public class BuyerStep3View extends TradeStepView { protected void addInfoBlock() { addTitledGroupBg(gridPane, ++gridRow, 2, getInfoBlockTitle(), Layout.GROUP_DISTANCE); infoLabel = addMultilineLabel(gridPane, gridRow, "", Layout.FIRST_ROW_AND_GROUP_DISTANCE); + GridPane.setColumnSpan(infoLabel, 2); textFieldWithIcon = addTopLabelTextFieldWithIcon(gridPane, ++gridRow, Res.get("portfolio.pending.step3_buyer.wait.msgStateInfo.label"), 0).second; } @@ -112,6 +114,7 @@ public class BuyerStep3View extends TradeStepView { iconLabel.getStyleClass().add("trade-msg-state-stored"); break; case FAILED: + case NACKED: textFieldWithIcon.setIcon(AwesomeIcon.EXCLAMATION_SIGN); iconLabel.getStyleClass().add("trade-msg-state-acknowledged"); break; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index e589443301..9a328695df 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -142,7 +142,7 @@ public class BuyerStep4View extends TradeStepView { if (!DevEnv.isDevMode()) { UserThread.runAfter(() -> new Popup().headLine(Res.get("portfolio.pending.step5_buyer.tradeCompleted.headline")) .feedback(Res.get("portfolio.pending.step5_buyer.tradeCompleted.msg")) - .actionButtonTextWithGoTo("navigation.portfolio.closedTrades") + .actionButtonTextWithGoTo("portfolio.tab.history") .onAction(() -> model.dataModel.navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class)) .dontShowAgainId("tradeCompleteWithdrawCompletedInfo") .show(), 500, TimeUnit.MILLISECONDS); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java index 2ca41f1d42..9c23250888 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java @@ -34,7 +34,7 @@ public class SellerStep1View extends TradeStepView { @Override protected void onPendingTradesInitialized() { super.onPendingTradesInitialized(); - checkForUnconfirmedTimeout(); + //checkForUnconfirmedTimeout(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index c6fe0cab23..20e12a3f56 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -151,6 +151,9 @@ public class SellerStep3View extends TradeStepView { break; } } + + // update confirm button state + confirmButton.setDisable(!confirmPaymentReceivedPermitted()); }); } diff --git a/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java index ee147ad376..e2a67428d9 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/SettingsView.java @@ -56,9 +56,9 @@ public class SettingsView extends ActivatableView { @Override public void initialize() { - preferencesTab.setText(Res.get("settings.tab.preferences").toUpperCase()); - networkTab.setText(Res.get("settings.tab.network").toUpperCase()); - aboutTab.setText(Res.get("settings.tab.about").toUpperCase()); + preferencesTab.setText(Res.get("settings.tab.preferences")); + networkTab.setText(Res.get("settings.tab.network")); + aboutTab.setText(Res.get("settings.tab.about")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SettingsView.class) == 1) diff --git a/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java b/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java index e543837f5d..73e51a47da 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/about/AboutView.java @@ -19,6 +19,7 @@ package haveno.desktop.main.settings.about; import com.google.inject.Inject; import haveno.common.app.Version; +import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; @@ -35,16 +36,18 @@ import javafx.scene.layout.GridPane; @FxmlView public class AboutView extends ActivatableView { +private final FilterManager filterManager; private int gridRow = 0; @Inject - public AboutView() { + public AboutView(FilterManager filterManager) { super(); + this.filterManager = filterManager; } @Override public void initialize() { - addTitledGroupBg(root, gridRow, 4, Res.get("setting.about.aboutHaveno")); + addTitledGroupBg(root, gridRow, 5, Res.get("setting.about.aboutHaveno")); Label label = addLabel(root, gridRow, Res.get("setting.about.about"), Layout.TWICE_FIRST_ROW_DISTANCE); label.setWrapText(true); @@ -77,8 +80,11 @@ public class AboutView extends ActivatableView { if (isXmr) addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.feeEstimation.label"), "Monero node"); - addTitledGroupBg(root, ++gridRow, 2, Res.get("setting.about.versionDetails"), Layout.GROUP_DISTANCE); + String minVersion = filterManager.getDisableTradeBelowVersion() == null ? Res.get("shared.none") : filterManager.getDisableTradeBelowVersion(); + + addTitledGroupBg(root, ++gridRow, 3, Res.get("setting.about.versionDetails"), Layout.GROUP_DISTANCE); addCompactTopLabelTextField(root, gridRow, Res.get("setting.about.version"), Version.VERSION, Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + addCompactTopLabelTextField(root, ++gridRow, Res.get("filterWindow.disableTradeBelowVersion"), minVersion); addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.subsystems.label"), Res.get("setting.about.subsystems.val", diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml index 1f3e8840d7..d4dcf14085 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.fxml @@ -53,7 +53,7 @@ - + @@ -91,7 +91,7 @@ - + @@ -108,6 +108,9 @@ + + + diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 0773217cd1..4d1a6cde01 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -149,6 +149,14 @@ public class NetworkSettingsView extends ActivatableView { @Override public void initialize() { + GUIUtil.applyTableStyle(p2pPeersTableView); + GUIUtil.applyTableStyle(moneroConnectionsTableView); + + onionAddress.getStyleClass().add("label-float"); + sentDataTextField.getStyleClass().add("label-float"); + receivedDataTextField.getStyleClass().add("label-float"); + chainHeightTextField.getStyleClass().add("label-float"); + btcHeader.setText(Res.get("settings.net.xmrHeader")); p2pHeader.setText(Res.get("settings.net.p2pHeader")); onionAddress.setPromptText(Res.get("settings.net.onionAddressLabel")); @@ -160,7 +168,6 @@ public class NetworkSettingsView extends ActivatableView { useTorForXmrOnRadio.setText(Res.get("settings.net.useTorForXmrOnRadio")); moneroNodesLabel.setText(Res.get("settings.net.moneroNodesLabel")); moneroConnectionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); - moneroConnectionAddressColumn.getStyleClass().add("first-column"); moneroConnectionConnectedColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connection"))); localhostXmrNodeInfoLabel.setText(Res.get("settings.net.localhostXmrNodeInfo")); useProvidedNodesRadio.setText(Res.get("settings.net.useProvidedNodesRadio")); @@ -170,7 +177,6 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsButton.updateText(Res.get("settings.net.rescanOutputsButton")); p2PPeersLabel.setText(Res.get("settings.net.p2PPeersLabel")); onionAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.onionAddressColumn"))); - onionAddressColumn.getStyleClass().add("first-column"); creationDateColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.creationDateColumn"))); connectionTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connectionTypeColumn"))); sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); @@ -180,7 +186,6 @@ public class NetworkSettingsView extends ActivatableView { sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); peerTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.peerTypeColumn"))); - peerTypeColumn.getStyleClass().add("last-column"); openTorSettingsButton.updateText(Res.get("settings.net.openTorSettingsButton")); // TODO: hiding button to rescan outputs until supported @@ -275,7 +280,7 @@ public class NetworkSettingsView extends ActivatableView { showShutDownPopup(); } }; - filterPropertyListener = (observable, oldValue, newValue) -> applyPreventPublicXmrNetwork(); + filterPropertyListener = (observable, oldValue, newValue) -> applyFilter(); // disable radio buttons if no nodes available if (xmrNodes.getProvidedXmrNodes().isEmpty()) { @@ -298,7 +303,7 @@ public class NetworkSettingsView extends ActivatableView { moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener); if (filterManager.getFilter() != null) - applyPreventPublicXmrNetwork(); + applyFilter(); filterManager.filterProperty().addListener(filterPropertyListener); @@ -492,7 +497,9 @@ public class NetworkSettingsView extends ActivatableView { } - private void applyPreventPublicXmrNetwork() { + private void applyFilter() { + + // prevent public xmr network final boolean preventPublicXmrNetwork = isPreventPublicXmrNetwork(); usePublicNodesRadio.setDisable(isPublicNodesDisabled()); if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) { diff --git a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java index da71490b9a..c0e0862fd0 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/preferences/PreferencesView.java @@ -17,7 +17,6 @@ package haveno.desktop.main.settings.preferences; -import static com.google.common.base.Preconditions.checkArgument; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.UserThread; @@ -40,6 +39,7 @@ import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.XmrValidator; import haveno.core.trade.HavenoUtils; +import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; @@ -47,7 +47,6 @@ import haveno.core.util.ParsingUtils; import haveno.core.util.validation.IntegerValidator; import haveno.core.util.validation.RegexValidator; import haveno.core.util.validation.RegexValidatorFactory; -import haveno.core.xmr.wallet.Restrictions; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.AutoTooltipButton; @@ -102,7 +101,7 @@ import org.apache.commons.lang3.StringUtils; @FxmlView public class PreferencesView extends ActivatableViewAndModel { private final User user; - private TextField btcExplorerTextField; + private TextField xmrExplorerTextField; private ComboBox userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; @@ -112,8 +111,8 @@ public class PreferencesView extends ActivatableViewAndModel allCryptoCurrencies; private ObservableList tradeCurrencies; private InputTextField deviationInputTextField; - private ChangeListener deviationListener, ignoreTradersListListener, ignoreDustThresholdListener, - rpcUserListener, rpcPwListener, blockNotifyPortListener, + private ChangeListener deviationListener, ignoreTradersListListener, + rpcUserListener, rpcPwListener, blockNotifyPortListener, clearDataAfterDaysListener, autoConfTradeLimitListener, autoConfServiceAddressListener; private ChangeListener deviationFocusedListener; private final boolean displayStandbyModeFeature; @@ -186,6 +185,24 @@ public class PreferencesView extends ActivatableViewAndModel { + DontShowAgainLookup.dontShowAgain(key, true); + // user has acknowledged, enable the feature with a reasonable default value + preferences.setClearDataAfterDays(Preferences.CLEAR_DATA_AFTER_DAYS_DEFAULT); + clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } + // We want to have it updated in case an asset got removed allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager)); allCryptoCurrencies.removeAll(cryptoCurrencies); @@ -220,9 +237,9 @@ public class PreferencesView extends ActivatableViewAndModel btcExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); - btcExplorerTextField = btcExp.first; - editCustomBtcExplorer = btcExp.second; + Tuple2 xmrExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); + xmrExplorerTextField = xmrExp.first; + editCustomBtcExplorer = xmrExp.second; // deviation deviationInputTextField = addInputTextField(root, ++gridRow, @@ -259,22 +276,17 @@ public class PreferencesView extends ActivatableViewAndModel { + // clearDataAfterDays + clearDataAfterDaysInputTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.clearDataAfterDays")); + IntegerValidator clearDataAfterDaysValidator = new IntegerValidator(); + clearDataAfterDaysValidator.setMinValue(1); + clearDataAfterDaysValidator.setMaxValue(Preferences.CLEAR_DATA_AFTER_DAYS_DISABLED); + clearDataAfterDaysInputTextField.setValidator(clearDataAfterDaysValidator); + clearDataAfterDaysListener = (observable, oldValue, newValue) -> { try { int value = Integer.parseInt(newValue); - checkArgument(value >= Restrictions.getMinNonDustOutput().value, - "Input must be at least " + Restrictions.getMinNonDustOutput().value); - checkArgument(value <= 2000, - "Input must not be higher than 2000 Satoshis"); if (!newValue.equals(oldValue)) { - preferences.setIgnoreDustThreshold(value); + preferences.setClearDataAfterDays(value); } } catch (Throwable ignore) { } @@ -287,7 +299,7 @@ public class PreferencesView extends ActivatableViewAndModel() { @Override public String toString(TradeCurrency object) { - return object.getCode() + " - " + object.getName(); + return object.getName() + " (" + object.getCode() + ")"; } @Override @@ -636,7 +648,7 @@ public class PreferencesView extends ActivatableViewAndModel referralIdInputTextField.setText(referralId)); referralIdInputTextField.setPromptText(Res.get("setting.preferences.refererId.prompt"));*/ - ignoreDustThresholdInputTextField.setText(String.valueOf(preferences.getIgnoreDustThreshold())); + clearDataAfterDaysInputTextField.setText(String.valueOf(preferences.getClearDataAfterDays())); userLanguageComboBox.setItems(languageCodes); userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage()); userLanguageComboBox.setConverter(new StringConverter<>() { @@ -688,7 +700,7 @@ public class PreferencesView extends ActivatableViewAndModel { preferences.setBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); - btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + xmrExplorerTextField.setText(preferences.getBlockChainExplorer().name); }) .closeButtonText(Res.get("shared.cancel")) .onClose(urlWindow::hide) @@ -830,7 +842,7 @@ public class PreferencesView extends ActivatableViewAndModel { super.updateItem(message, empty); if (message != null && !empty) { - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(messageLabel.getText())); + copyLabel.setOnMouseClicked(e -> { + Utilities.copyToClipboard(messageLabel.getText()); + Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); + Node node = (Node) e.getSource(); + UserThread.runAfter(() -> tp.hide(), 1); + tp.show(node, e.getScreenX() + Layout.PADDING, e.getScreenY() + Layout.PADDING); + }); messageLabel.setOnMouseClicked(event -> { if (2 > event.getClickCount()) { return; @@ -319,7 +327,7 @@ public class ChatView extends AnchorPane { AnchorPane.clearConstraints(headerLabel); AnchorPane.clearConstraints(arrow); AnchorPane.clearConstraints(messageLabel); - AnchorPane.clearConstraints(copyIcon); + AnchorPane.clearConstraints(copyLabel); AnchorPane.clearConstraints(statusHBox); AnchorPane.clearConstraints(attachmentsBox); @@ -328,7 +336,7 @@ public class ChatView extends AnchorPane { AnchorPane.setTopAnchor(headerLabel, 0d); AnchorPane.setBottomAnchor(arrow, bottomBorder + 5d); AnchorPane.setTopAnchor(messageLabel, 25d); - AnchorPane.setTopAnchor(copyIcon, 25d); + AnchorPane.setTopAnchor(copyLabel, 25d); AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); @@ -341,20 +349,20 @@ public class ChatView extends AnchorPane { headerLabel.getStyleClass().removeAll("message-header", "my-message-header", "success-text", "highlight-static"); messageLabel.getStyleClass().removeAll("my-message", "message"); - copyIcon.getStyleClass().removeAll("my-message", "message"); + copyLabel.getStyleClass().removeAll("my-message", "message"); if (message.isSystemMessage()) { headerLabel.getStyleClass().addAll("message-header", "success-text"); bg.setId("message-bubble-green"); messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); + copyLabel.getStyleClass().add("my-message"); message.addWeakMessageStateListener(() -> UserThread.execute(() -> updateMsgState(message))); updateMsgState(message); } else if (isMyMsg) { headerLabel.getStyleClass().add("my-message-header"); bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); + copyLabel.getStyleClass().add("my-message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_blue_left"); else @@ -375,7 +383,7 @@ public class ChatView extends AnchorPane { headerLabel.getStyleClass().add("message-header"); bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); - copyIcon.getStyleClass().add("message"); + copyLabel.getStyleClass().add("message"); if (supportSession.isClient()) arrow.setId("bubble_arrow_grey_right"); else @@ -389,7 +397,7 @@ public class ChatView extends AnchorPane { AnchorPane.setRightAnchor(bg, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); - AnchorPane.setRightAnchor(copyIcon, padding); + AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setLeftAnchor(statusHBox, padding); @@ -400,7 +408,7 @@ public class ChatView extends AnchorPane { AnchorPane.setLeftAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding + arrowWidth); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight); - AnchorPane.setRightAnchor(copyIcon, padding); + AnchorPane.setRightAnchor(copyLabel, padding); AnchorPane.setLeftAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setRightAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(statusHBox, padding); @@ -411,7 +419,7 @@ public class ChatView extends AnchorPane { AnchorPane.setRightAnchor(arrow, border); AnchorPane.setLeftAnchor(messageLabel, padding); AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight + arrowWidth); - AnchorPane.setRightAnchor(copyIcon, padding + arrowWidth); + AnchorPane.setRightAnchor(copyLabel, padding + arrowWidth); AnchorPane.setLeftAnchor(attachmentsBox, padding); AnchorPane.setRightAnchor(attachmentsBox, padding + arrowWidth); AnchorPane.setLeftAnchor(statusHBox, padding); @@ -454,8 +462,9 @@ public class ChatView extends AnchorPane { } // Need to set it here otherwise style is not correct - AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY, "16.0"); - copyIcon.getStyleClass().addAll("icon", "copy-icon-disputes"); + copyLabel.getStyleClass().addAll("icon", "copy-icon-disputes"); + MaterialDesignIconView copyIcon = new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "16.0"); + copyLabel.setGraphic(copyIcon); // TODO There are still some cell rendering issues on updates setGraphic(messageAnchorPane); @@ -465,7 +474,7 @@ public class ChatView extends AnchorPane { messageAnchorPane.prefWidthProperty().unbind(); - copyIcon.setOnMouseClicked(null); + copyLabel.setOnMouseClicked(null); messageLabel.setOnMouseClicked(null); setGraphic(null); } diff --git a/desktop/src/main/java/haveno/desktop/main/support/SupportView.java b/desktop/src/main/java/haveno/desktop/main/support/SupportView.java index 42503f315f..12a000c00b 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/SupportView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/SupportView.java @@ -139,9 +139,9 @@ public class SupportView extends ActivatableView { // Has to be called before loadView updateAgentTabs(); - tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support").toUpperCase()); - tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support").toUpperCase()); - tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); + tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support")); + tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support")); + tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support")); navigationListener = (viewPath, data) -> { if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) @@ -221,16 +221,16 @@ public class SupportView extends ActivatableView { // We might get that method called before we have the map is filled in the arbitratorManager if (arbitratorTab != null) { - arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator")).toUpperCase()); + arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator"))); } if (signedOfferTab != null) { - signedOfferTab.setText(Res.get("support.tab.SignedOffers").toUpperCase()); + signedOfferTab.setText(Res.get("support.tab.SignedOffers")); } if (mediatorTab != null) { - mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase()); + mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator"))); } if (refundAgentTab != null) { - refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgentForSupportStaff")).toUpperCase()); + refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgentForSupportStaff"))); } } diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java index 5ac5f03328..87c05c997c 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java @@ -223,12 +223,8 @@ public abstract class DisputeView extends ActivatableView implements @Override public void initialize() { - Label label = new AutoTooltipLabel(Res.get("support.filter")); - HBox.setMargin(label, new Insets(5, 0, 0, 0)); - HBox.setHgrow(label, Priority.NEVER); - filterTextField = new InputTextField(); - filterTextField.setPromptText(Res.get("support.filter.prompt")); + filterTextField.setPromptText(Res.get("shared.filter")); Tooltip tooltip = new Tooltip(); tooltip.setShowDelay(Duration.millis(100)); tooltip.setShowDuration(Duration.seconds(10)); @@ -298,8 +294,7 @@ public abstract class DisputeView extends ActivatableView implements HBox filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, - filterTextField, + filterBox.getChildren().addAll(filterTextField, alertIconLabel, spacer, reOpenButton, @@ -311,6 +306,7 @@ public abstract class DisputeView extends ActivatableView implements VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); VBox.setVgrow(tableView, Priority.SOMETIMES); tableView.setMinHeight(150); @@ -739,11 +735,13 @@ public abstract class DisputeView extends ActivatableView implements .append(winner) .append(")\n"); - String buyerPaymentAccountPayload = Utilities.toTruncatedString( - firstDispute.getBuyerPaymentAccountPayload().getPaymentDetails(). + String buyerPaymentAccountPayload = firstDispute.getBuyerPaymentAccountPayload() == null ? null : + Utilities.toTruncatedString( + firstDispute.getBuyerPaymentAccountPayload().getPaymentDetails(). replace("\n", " ").replace(";", "."), 100); - String sellerPaymentAccountPayload = Utilities.toTruncatedString( - firstDispute.getSellerPaymentAccountPayload().getPaymentDetails() + String sellerPaymentAccountPayload = firstDispute.getSellerPaymentAccountPayload() == null ? null : + Utilities.toTruncatedString( + firstDispute.getSellerPaymentAccountPayload().getPaymentDetails() .replace("\n", " ").replace(";", "."), 100); String buyerNodeAddress = contract.getBuyerNodeAddress().getFullAddress(); String sellerNodeAddress = contract.getSellerNodeAddress().getFullAddress(); @@ -955,7 +953,6 @@ public abstract class DisputeView extends ActivatableView implements { setMaxWidth(80); setMinWidth(65); - getStyleClass().addAll("first-column", "avatar-column"); setSortable(false); } }; @@ -1352,7 +1349,6 @@ public abstract class DisputeView extends ActivatableView implements setMinWidth(50); } }; - column.getStyleClass().add("last-column"); column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java index 10c657dce7..5901d48769 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -208,7 +208,6 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo protected void setupTable() { super.setupTable(); - stateColumn.getStyleClass().remove("last-column"); tableView.getColumns().add(getAlertColumn()); } @@ -243,7 +242,6 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo setMinWidth(50); } }; - column.getStyleClass().add("last-column"); column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( c -> new TableCell<>() { diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index d045e3b5b0..aaa93fc35f 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -1,4 +1,6 @@ .root { + -bs-color-primary: rgb(28, 96, 220); + /* javafx main color palette */ -fx-base: #29292a; -fx-background: #29292a; @@ -9,7 +11,7 @@ -fx-text-fill: #dadada; /* javafx elements */ - -fx-accent: #0b65da; + -fx-accent: -bs-color-primary; -fx-box-border: transparent; -fx-focus-color: #0c59bd; -fx-faint-focus-color: #0c59bd; @@ -18,12 +20,11 @@ -fx-default-button: derive(-fx-accent, 95%); /* haveno main colors */ - -bs-color-primary: #0b65da; -bs-color-primary-dark: #0c59bd; - -bs-text-color: #dadada; - -bs-background-color: #29292a; - -bs-background-gray: #2B2B2B; - -bs-content-background-gray: #1F1F1F; + -bs-text-color: white; + -bs-background-color: black; + -bs-background-gray: transparent; + -bs-content-background-gray: black; /* fifty shades of gray */ -bs-color-gray-13: #bbb; @@ -43,7 +44,19 @@ -bs-color-gray-bbb: #5a5a5a; -bs-color-gray-aaa: #29292a; -bs-color-gray-fafa: #0a0a0a; - -bs-color-gray-background: #1F1F1F; + -bs-color-gray-background: black; + -bs-color-background-popup: rgb(38, 38, 38); + -bs-color-background-popup-blur: rgb(9, 9, 9); + -bs-color-background-popup-input: rgb(9, 9, 9); + -bs-color-background-form-field: rgb(26, 26, 26); + -bs-color-background-form-field-readonly: rgb(18, 18, 18); + -bs-color-border-form-field: rgb(65, 65, 65); + -bs-color-background-pane: rgb(15, 15, 15); + -bs-color-background-row-even: rgb(19, 19, 19); + -bs-color-background-row-odd: rgb(9, 9, 9); + -bs-color-table-cell-dim: -bs-color-gray-ccc; + -bs-text-color-dim1: rgb(87, 87, 87); + -bs-text-color-dim2: rgb(130, 130, 130); /* lesser used colors */ -bs-color-blue-5: #0a4576; @@ -70,11 +83,15 @@ -bs-rd-nav-border: #535353; -bs-rd-nav-primary-border: rgba(0, 0, 0, 0); -bs-rd-nav-border-color: rgba(255, 255, 255, 0.1); - -bs-rd-nav-background: #141414; - -bs-rd-nav-primary-background: rgba(255, 255, 255, 0.015); - -bs-rd-nav-selected: #fff; - -bs-rd-nav-deselected: rgba(255, 255, 255, 0.45); - -bs-rd-nav-button-hover: rgba(255, 255, 255, 0.03); + -bs-rd-nav-background: rgb(15, 15, 15); + -bs-rd-nav-primary-background: rgb(15, 15, 15); + -bs-rd-nav-selected: black; + -bs-rd-nav-deselected: rgba(255, 255, 255, 1); + -bs-rd-nav-secondary-selected: -fx-accent; + -bs-rd-nav-secondary-deselected: -bs-rd-font-light; + -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); + -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); + -bs-rd-nav-hover-text: black; -bs-content-pane-bg-top: #212121; -bs-rd-tab-border: rgba(255, 255, 255, 0.00); @@ -90,7 +107,7 @@ -bs-footer-pane-text: #cfcecf; -bs-footer-pane-line: #29292a; - -bs-rd-font-balance: #bbbbbb; + -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #d4d4d4; -bs-rd-font-dark: #cccccc; -bs-rd-font-light: #b4b4b4; @@ -99,28 +116,28 @@ -bs-rd-font-confirmation-label: #504f52; -bs-rd-font-balance-label: #999999; - -bs-text-color-transparent-dark: rgba(29, 29, 33, 0.54); + -bs-text-color-dropshadow: rgba(45, 45, 49, .75); + -bs-text-color-dropshadow-light-mode: transparent; -bs-text-color-transparent: rgba(29, 29, 33, 0.2); -bs-color-gray-line: #504f52; -bs-rd-separator: #1F1F1F; - -bs-rd-separator-dark: #1F1F1F; + -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #d83431; -bs-rd-error-field: #521C1C; -bs-rd-message-bubble: #0086c6; -bs-rd-tooltip-truncated: #afaeb0; - -bs-toggle-selected: #25b135; + /*-bs-toggle-selected: rgb(12, 89, 189);*/ + -bs-toggle-selected: rgb(12, 89, 190); -bs-warning: #db6300; - -bs-buy: #006600; - -bs-buy-focus: black; - -bs-buy-hover: #237b2d; - -bs-buy-transparent: rgba(46, 163, 60, 0.3); - -bs-sell: #660000; - -bs-sell-focus: #090202; - -bs-sell-hover: #b42522; - -bs-sell-transparent: rgba(216, 52, 49, 0.3); - -bs-volume-transparent: rgba(37, 177, 54, 0.5); + -bs-buy: rgb(80, 180, 90); + -bs-buy-focus: derive(-bs-buy, -50%); + -bs-buy-hover: derive(-bs-buy, -10%); + -bs-sell: rgb(213, 63, 46); + -bs-sell-focus: derive(-bs-sell, -50%); + -bs-sell-hover: derive(-bs-sell, -10%); + -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: rgba(21, 188, 29, 0.8); -bs-candle-stick-loss: #ee6563; -bs-candle-stick-won: #15bc1d; @@ -154,7 +171,7 @@ /* Monero orange color code */ -xmr-orange: #f26822; - -bs-support-chat-background: #cccccc; + -bs-support-chat-background: rgb(125, 125, 125); } /* table view */ @@ -164,7 +181,7 @@ } .table-view .column-header { - -fx-background-color: derive(-bs-background-color,-50%); + -fx-background-color: -bs-color-background-pane; -fx-border-width: 0; } @@ -173,21 +190,31 @@ -fx-border-width: 0; } +/** These must be set to override default styles */ .table-view .table-row-cell:even .table-cell { - -fx-background-color: derive(-bs-background-color, -5%); - -fx-border-color: derive(-bs-background-color, -5%); + -fx-background-color: -bs-color-background-row-even; + -fx-border-color: -bs-color-background-row-even; } - .table-view .table-row-cell:odd .table-cell { - -fx-background-color: derive(-bs-background-color,-30%); - -fx-border-color: derive(-bs-background-color,-30%); + -fx-background-color: -bs-color-background-row-odd; + -fx-border-color: -bs-color-background-row-odd; } - .table-view .table-row-cell:selected .table-cell { -fx-background: -fx-accent; -fx-background-color: -fx-selection-bar; -fx-border-color: -fx-selection-bar; } +.table-view .table-row-cell:selected .table-cell, +.table-view .table-row-cell:selected .table-cell .label, +.table-view .table-row-cell:selected .table-cell .text { + -fx-text-fill: -fx-dark-text-color; +} +.table-view .table-row-cell:selected .table-cell .hyperlink, +.table-view .table-row-cell:selected .table-cell .hyperlink .text, +.table-view .table-row-cell:selected .table-cell .hyperlink-with-icon, +.table-view .table-row-cell:selected .table-cell .hyperlink-with-icon .text { + -fx-fill: -fx-dark-text-color; +} .table-row-cell { -fx-border-color: -bs-background-color; @@ -208,35 +235,49 @@ -fx-background-color: -bs-tab-content-area; } -.jfx-tab-pane .viewport { - -fx-background-color: -bs-viewport-background; + +.jfx-tab-pane .headers-region .tab:selected .tab-container .tab-label { + -fx-text-fill: white; } -.jfx-tab-pane .tab-header-background { - -fx-background-color: derive(-bs-color-gray-background, -20%); +.nav-secondary-button:selected .text { + -fx-fill: white; +} + +.jfx-tab-pane .headers-region > .tab > .jfx-rippler { + -jfx-rippler-fill: none; +} + +.jfx-tab-pane .viewport { + -fx-background-color: -bs-viewport-background; } /* text field */ .jfx-text-field, .jfx-text-area, .jfx-combo-box, .jfx-combo-box > .list-cell { - -fx-background-color: derive(-bs-background-color, 15%); -fx-prompt-text-fill: -bs-color-gray-6; -fx-text-fill: -bs-color-gray-12; } -.jfx-text-area:readonly, .jfx-text-field:readonly, +.jfx-text-area:readonly, +.jfx-text-field:readonly, .hyperlink-with-icon { -fx-background: -bs-background-color; - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-form-field-readonly; -fx-prompt-text-fill: -bs-color-gray-2; -fx-text-fill: -bs-color-gray-3; } + +.popover > .content .text-field { + -fx-background-color: -bs-color-background-form-field !important; +} + .jfx-combo-box > .text, .jfx-text-field-top-label, .jfx-text-area-top-label { -fx-text-fill: -bs-color-gray-11; } -.input-with-border { +.offer-input { -fx-border-color: -bs-color-gray-2; -fx-border-width: 0 0 10 0; } @@ -254,11 +295,6 @@ -fx-text-fill: -fx-dark-text-color; } -.chart-pane, .chart-plot-background, -#charts .chart-plot-background, -#charts-dao .chart-plot-background { - -fx-background-color: transparent; -} .axis:top, .axis:right, .axis:bottom, .axis:left { -fx-border-color: transparent transparent transparent transparent; } @@ -332,7 +368,7 @@ } .combo-box-popup > .list-view{ - -fx-background-color: -bs-background-color; + -fx-background-color: -bs-color-background-pane; } .jfx-combo-box > .arrow-button > .arrow { @@ -352,7 +388,6 @@ } .list-view .list-cell:odd, .list-view .list-cell:even { - -fx-background-color: -bs-background-color; -fx-border-width: 0; } @@ -371,18 +406,6 @@ -fx-border-width: 0; } -.jfx-text-field { - -fx-background-radius: 4; -} - -.jfx-text-field > .input-line { - -fx-translate-x: 0; -} - -.jfx-text-field > .input-focused-line { - -fx-translate-x: 0; -} - .jfx-text-field-top-label { -fx-text-fill: -bs-color-gray-dim; } @@ -394,14 +417,13 @@ -fx-background-color: derive(-bs-background-color, 15%); } .jfx-combo-box:error, -.jfx-text-field:error{ +.jfx-text-field:error { -fx-text-fill: -bs-rd-error-red; -fx-background-color: -bs-rd-error-field; } .jfx-combo-box:error:focused, .jfx-text-field:error:focused{ - -fx-text-fill: -bs-rd-error-red; -fx-background-color: derive(-bs-rd-error-field, -5%); } @@ -417,11 +439,7 @@ -jfx-disable-animation: true; } -.jfx-password-field { - -fx-background-color: derive(-bs-background-color, -15%); -} - -.input-with-border { +.offer-input { -fx-border-width: 0; -fx-border-color: -bs-background-color; } @@ -448,11 +466,6 @@ -jfx-disable-animation: true; } -.top-navigation { - -fx-border-width: 0 0 0 0; - -fx-padding: 0 7 0 0; -} - .nav-price-balance { -fx-effect: null; } @@ -462,37 +475,17 @@ } .nav-button:selected { - -fx-background-color: derive(-bs-color-primary-dark, -10%); + -fx-background-color: white; -fx-effect: null; } -.nav-button:hover { - -fx-background-color: -bs-rd-nav-button-hover; -} - .nav-primary .nav-button:selected { - -fx-background-color: derive(-bs-color-primary-dark, -5%); + -fx-background-color: derive(white, -5%); } .table-view { -fx-border-color: transparent; } -.table-view .table-cell { - -fx-padding: 6 0 4 0; - -fx-text-fill: -bs-text-color; -} -.table-view .table-cell.last-column { - -fx-padding: 6 10 4 0; -} - -.table-view .table-cell.last-column.avatar-column { - -fx-padding: 6 0 4 0; -} - -.table-view .table-cell.first-column { - -fx-padding: 6 0 4 10; -} - .jfx-tab-pane .headers-region .tab .tab-container .tab-label { -fx-cursor: hand; -jfx-disable-animation: true; @@ -559,12 +552,99 @@ } .toggle-button-no-slider { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; - -fx-background-radius: 3; - -fx-background-insets: 0, 1; + -fx-background-color: -bs-color-background-form-field; } .toggle-button-no-slider:selected { + -fx-text-fill: white; + -fx-background-color: -bs-color-gray-ccc; + -fx-border-color: -bs-color-gray-ccc; + -fx-border-width: 1px; +} + +.toggle-button-no-slider:hover { + -fx-cursor: hand; + -fx-background-color: -bs-color-gray-ddd; + -fx-border-color: -bs-color-gray-ddd; +} + +.toggle-button-no-slider:selected:hover { + -fx-cursor: hand; + -fx-background-color: -bs-color-gray-3; + -fx-border-color: -bs-color-gray-3; +} + +.toggle-button-no-slider:pressed, .toggle-button-no-slider:selected:hover:pressed { -fx-background-color: -bs-color-gray-bbb; } + +#image-logo-splash { + -fx-image: url("../../images/logo_splash_dark_mode.png"); +} + +#image-logo-splash-testnet { + -fx-image: url("../../images/logo_splash_testnet_dark_mode.png"); +} + +#image-logo-landscape { + -fx-image: url("../../images/logo_landscape_dark_mode.png"); +} + +.table-view .placeholder { + -fx-background-color: -bs-color-background-pane; +} + +#charts .default-color0.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(80, 181, 90, 0.45) 0%, + rgba(80, 181, 90, 0.0) 100% + ); +} + +#charts .default-color1.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(213, 63, 46, 0.45) 0%, + rgba(213, 63, 46, 0.0) 100% + ); +} + +.table-view .table-row-cell .label { + -fx-text-fill: -bs-text-color; +} + +.table-view.non-interactive-table .table-cell, +.table-view.non-interactive-table .table-cell .label, +.table-view.non-interactive-table .label, +.table-view.non-interactive-table .text, +.table-view.non-interactive-table .hyperlink, +.table-view.non-interactive-table .hyperlink-with-icon, +.table-view.non-interactive-table .table-row-cell .hyperlink .text { + -fx-text-fill: -bs-color-gray-dim; +} + +.table-view.non-interactive-table .hyperlink, +.table-view.non-interactive-table .hyperlink-with-icon, +.table-view.non-interactive-table .table-row-cell .hyperlink .text { + -fx-fill: -bs-color-gray-dim; +} + +.table-view.non-interactive-table .table-cell.highlight-text, +.table-view.non-interactive-table .table-cell.highlight-text .label, +.table-view.non-interactive-table .table-cell.highlight-text .text, +.table-view.non-interactive-table .table-cell.highlight-text .hyperlink, +.table-view.non-interactive-table .table-cell.highlight-text .hyperlink .text { + -fx-text-fill: -fx-dark-text-color; +} + +/* Match specificity to override. */ +.table-view.non-interactive-table .table-cell.highlight-text .zero-decimals { + -fx-text-fill: -bs-color-gray-3; +} + +.regular-text-color { + -fx-text-fill: -bs-text-color; +} + +#image-fiat-logo { + -fx-image: url("../../images/fiat_logo_dark_mode.png"); +} diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 7605eb1819..8542066595 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -1,5 +1,5 @@ .root { - -bs-color-primary: #0b65da; + -bs-color-primary: rgb(28, 96, 220); -bs-color-primary-dark: #0c59bd; -bs-text-color: #000000; -bs-background-color: #ffffff; @@ -38,15 +38,20 @@ -bs-yellow-light: derive(-bs-yellow, 81%); -bs-blue-transparent: #0f87c344; -bs-bg-green: #99ba9c; - -bs-rd-green: #0b65da; + -bs-rd-green: -bs-color-primary; -bs-rd-green-dark: #3EA34A; - -bs-rd-nav-selected: #0b65da; - -bs-rd-nav-deselected: rgba(255, 255, 255, 0.75); - -bs-rd-nav-background: #0c59bd; - -bs-rd-nav-primary-background: #0b65da; - -bs-rd-nav-primary-border: #0B65DA; + -bs-rd-nav-selected: -bs-color-primary; + -bs-rd-nav-deselected: rgba(255, 255, 255, 1); + -bs-rd-nav-secondary-selected: -fx-accent; + -bs-rd-nav-secondary-deselected: -bs-rd-font-light; + -bs-rd-nav-background: -bs-color-primary; + -bs-rd-nav-primary-background: -bs-color-primary; + -bs-rd-nav-button-hover: derive(-bs-rd-nav-background, 10%); + -bs-rd-nav-primary-button-hover: derive(-bs-rd-nav-primary-background, 10%); + -bs-rd-nav-primary-border: -bs-color-primary; -bs-rd-nav-border: #535353; -bs-rd-nav-border-color: rgba(255, 255, 255, 0.31); + -bs-rd-nav-hover-text: white; -bs-rd-tab-border: #e2e0e0; -bs-tab-content-area: #ffffff; -bs-color-gray-background: #f2f2f2; @@ -58,39 +63,38 @@ -bs-footer-pane-background: #dddddd; -bs-footer-pane-text: #4b4b4b; -bs-footer-pane-line: #bbb; - -bs-rd-font-balance: #4f4f4f; + -bs-rd-font-balance: white; -bs-rd-font-dark-gray: #3c3c3c; -bs-rd-font-dark: #4b4b4b; -bs-rd-font-light: #8d8d8d; -bs-rd-font-lighter: #a7a7a7; -bs-rd-font-confirmation-label: #504f52; - -bs-rd-font-balance-label: #8e8e8e; - -bs-text-color-transparent-dark: rgba(0, 0, 0, 0.54); + -bs-rd-font-balance-label: rgb(215, 215, 215, 1); + -bs-text-color-dropshadow: rgba(0, 0, 0, 0.54); + -bs-text-color-dropshadow-light-mode: rgba(0, 0, 0, 0.54); -bs-text-color-transparent: rgba(0, 0, 0, 0.2); -bs-color-gray-line: #979797; -bs-rd-separator: #dbdbdb; - -bs-rd-separator-dark: #d5e0d6; + -bs-rd-separator-dark: rgb(255, 255, 255, 0.1); -bs-rd-error-red: #dd0000; -bs-rd-message-bubble: #0086c6; -bs-toggle-selected: #7b7b7b; -bs-rd-tooltip-truncated: #0a0a0a; -bs-warning: #ff8a2b; - -bs-buy: #3ea34a; + -bs-buy: rgb(80, 180, 90); -bs-buy-focus: derive(-bs-buy, -50%); -bs-buy-hover: derive(-bs-buy, -10%); - -bs-buy-transparent: rgba(62, 163, 74, 0.3); - -bs-sell: #d73030; + -bs-sell: rgb(213, 63, 46); -bs-sell-focus: derive(-bs-sell, -50%); -bs-sell-hover: derive(-bs-sell, -10%); - -bs-sell-transparent: rgba(215, 48, 48, 0.3); - -bs-volume-transparent: rgba(37, 177, 53, 0.3); + -bs-volume-transparent: -bs-buy; -bs-candle-stick-average-line: -bs-rd-green; -bs-candle-stick-loss: #fe3001; -bs-candle-stick-won: #20b221; -bs-cancel: #dddddd; -bs-cancel-focus: derive(-bs-cancel, -50%); -bs-cancel-hover: derive(-bs-cancel, -10%); - -fx-accent: #0b65da; + -fx-accent: -bs-color-primary; -fx-box-border: #e9e9e9; -bs-green-soft: derive(-bs-rd-green, 60%); -bs-red-soft: derive(-bs-rd-error-red, 60%); @@ -104,6 +108,19 @@ -bs-prompt-text: -fx-control-inner-background; -bs-soft-red: #aa4c3b; -bs-turquoise-light: #11eeee; + -bs-color-border-form-field: -bs-background-gray; + -bs-color-background-form-field-readonly: -bs-color-gray-1; + -bs-color-background-pane: -bs-background-color; + -bs-color-background-row-even: -bs-color-background-pane; + -bs-color-background-row-odd: derive(-bs-color-background-pane, -6%); + -bs-color-table-cell-dim: -bs-color-gray-ccc; + -bs-color-background-popup: white; + -bs-color-background-popup-blur: white; + -bs-color-background-popup-input: -bs-color-gray-background; + -bs-color-background-form-field: white; + -bs-text-color-dim1: black; + -bs-text-color-dim2: black; + /* Monero orange color code */ -xmr-orange: #f26822; @@ -126,7 +143,33 @@ -fx-background-color: -bs-color-gray-3; } -.toggle-button-no-slider { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; +#image-logo-splash { + -fx-image: url("../../images/logo_splash_light_mode.png"); +} + +#image-logo-splash-testnet { + -fx-image: url("../../images/logo_splash_testnet_light_mode.png"); +} + +#image-logo-landscape { + -fx-image: url("../../images/logo_landscape_light_mode.png"); +} + +#charts .default-color0.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(62, 163, 74, 0.45) 0%, + rgba(62, 163, 74, 0.0) 100% + ); +} + +#charts .default-color1.chart-series-area-fill { + -fx-fill: linear-gradient(to bottom, + rgba(215, 48, 48, 0.45) 0%, + rgba(215, 48, 48, 0.0) 100% + ); +} + +/* All inputs have border in light mode. */ +.jfx-combo-box, .jfx-text-field, .jfx-text-area, .jfx-password-field { + -fx-border-color: -bs-color-border-form-field; } diff --git a/desktop/src/main/java/haveno/desktop/util/CssTheme.java b/desktop/src/main/java/haveno/desktop/util/CssTheme.java index 1e1c547607..4648d07eeb 100644 --- a/desktop/src/main/java/haveno/desktop/util/CssTheme.java +++ b/desktop/src/main/java/haveno/desktop/util/CssTheme.java @@ -58,6 +58,10 @@ public class CssTheme { scene.getStylesheets().add(cssThemeFolder + "theme-dev.css"); } + public static int getCurrentTheme() { + return currentCSSTheme; + } + public static boolean isDarkTheme() { return currentCSSTheme == CSS_THEME_DARK; } diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java index 3e68ccf876..17186603e8 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyList.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyList.java @@ -18,6 +18,8 @@ package haveno.desktop.util; import com.google.common.collect.Lists; + +import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TradeCurrency; import haveno.core.user.Preferences; import javafx.collections.FXCollections; @@ -63,6 +65,7 @@ public class CurrencyList { private List getPartitionedSortedItems(List currencies) { Map tradesPerCurrency = countTrades(currencies); + List fiatCurrencies = new ArrayList<>(); List traditionalCurrencies = new ArrayList<>(); List cryptoCurrencies = new ArrayList<>(); @@ -71,7 +74,9 @@ public class CurrencyList { Integer count = entry.getValue(); CurrencyListItem item = new CurrencyListItem(currency, count); - if (predicates.isTraditionalCurrency(currency)) { + if (predicates.isFiatCurrency(currency)) { + fiatCurrencies.add(item); + } else if (predicates.isTraditionalCurrency(currency)) { traditionalCurrencies.add(item); } @@ -81,10 +86,12 @@ public class CurrencyList { } Comparator comparator = getComparator(); + fiatCurrencies.sort(comparator); traditionalCurrencies.sort(comparator); cryptoCurrencies.sort(comparator); List result = new ArrayList<>(); + result.addAll(fiatCurrencies); result.addAll(traditionalCurrencies); result.addAll(cryptoCurrencies); @@ -92,14 +99,13 @@ public class CurrencyList { } private Comparator getComparator() { - Comparator result; if (preferences.isSortMarketCurrenciesNumerically()) { - Comparator byCount = Comparator.comparingInt(left -> left.numTrades); - result = byCount.reversed(); + return Comparator + .comparingInt((CurrencyListItem item) -> item.numTrades).reversed() + .thenComparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } else { - result = Comparator.comparing(item -> item.tradeCurrency); + return Comparator.comparing(item -> CurrencyUtil.isCryptoCurrency(item.tradeCurrency.getCode()) ? item.tradeCurrency.getName() : item.tradeCurrency.getCode()); } - return result; } private Map countTrades(List currencies) { diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java b/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java index 5a85a80d08..3aea50a638 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyListItem.java @@ -61,7 +61,7 @@ public class CurrencyListItem { if (isSpecialShowAllItem()) return Res.get(GUIUtil.SHOW_ALL_FLAG); else - return tradeCurrency.getCode() + " - " + tradeCurrency.getName(); + return tradeCurrency.getName() + " (" + tradeCurrency.getCode() + ")"; } private boolean isSpecialShowAllItem() { diff --git a/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java b/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java index a201bd6f36..15449dcb87 100644 --- a/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java +++ b/desktop/src/main/java/haveno/desktop/util/CurrencyPredicates.java @@ -25,6 +25,10 @@ class CurrencyPredicates { return CurrencyUtil.isCryptoCurrency(currency.getCode()); } + boolean isFiatCurrency(TradeCurrency currency) { + return CurrencyUtil.isFiatCurrency(currency.getCode()); + } + boolean isTraditionalCurrency(TradeCurrency currency) { return CurrencyUtil.isTraditionalCurrency(currency.getCode()); } diff --git a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java index 8c725188c1..d49f302b30 100644 --- a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java @@ -3,7 +3,6 @@ package haveno.desktop.util; import haveno.common.crypto.PubKeyRing; import haveno.core.account.witness.AccountAgeWitness; import haveno.core.account.witness.AccountAgeWitnessService; -import haveno.core.locale.CurrencyUtil; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import haveno.core.monetary.Price; @@ -118,10 +117,7 @@ public class DisplayUtils { /////////////////////////////////////////////////////////////////////////////////////////// public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()); - else - return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), currencyCode) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), currencyCode); + return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), Res.getBaseCurrencyCode()); } public static String getDirectionBothSides(OfferDirection direction, boolean isPrivate) { @@ -132,56 +128,31 @@ public class DisplayUtils { } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - String code = Res.getBaseCurrencyCode(); - return isMyOffer ? - Res.get("formatter.youAreAsMaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code) : - Res.get("formatter.youAreAsTaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code); - } else { - return isMyOffer ? - Res.get("formatter.youAreAsMaker", Res.get("shared.seller"), currencyCode, Res.get("shared.buyer"), currencyCode) : - Res.get("formatter.youAreAsTaker", Res.get("shared.seller"), currencyCode, Res.get("shared.buyer"), currencyCode); - } + String code = Res.getBaseCurrencyCode(); + return isMyOffer ? + Res.get("formatter.youAreAsMaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code) : + Res.get("formatter.youAreAsTaker", Res.get("shared.buyer"), code, Res.get("shared.seller"), code); } public static String getDirectionForSeller(boolean isMyOffer, String currencyCode) { - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - String code = Res.getBaseCurrencyCode(); - return isMyOffer ? - Res.get("formatter.youAreAsMaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code) : - Res.get("formatter.youAreAsTaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code); - } else { - return isMyOffer ? - Res.get("formatter.youAreAsMaker", Res.get("shared.buyer"), currencyCode, Res.get("shared.seller"), currencyCode) : - Res.get("formatter.youAreAsTaker", Res.get("shared.buyer"), currencyCode, Res.get("shared.seller"), currencyCode); - } + String code = Res.getBaseCurrencyCode(); + return isMyOffer ? + Res.get("formatter.youAreAsMaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code) : + Res.get("formatter.youAreAsTaker", Res.get("shared.seller"), code, Res.get("shared.buyer"), code); } public static String getDirectionForTakeOffer(OfferDirection direction, String currencyCode) { String baseCurrencyCode = Res.getBaseCurrencyCode(); - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - return direction == OfferDirection.BUY ? - Res.get("formatter.youAre", Res.get("shared.selling"), baseCurrencyCode, Res.get("shared.buying"), currencyCode) : - Res.get("formatter.youAre", Res.get("shared.buying"), baseCurrencyCode, Res.get("shared.selling"), currencyCode); - } else { - - return direction == OfferDirection.SELL ? - Res.get("formatter.youAre", Res.get("shared.selling"), currencyCode, Res.get("shared.buying"), baseCurrencyCode) : - Res.get("formatter.youAre", Res.get("shared.buying"), currencyCode, Res.get("shared.selling"), baseCurrencyCode); - } + return direction == OfferDirection.BUY ? + Res.get("formatter.youAre", Res.get("shared.selling"), baseCurrencyCode, Res.get("shared.buying"), currencyCode) : + Res.get("formatter.youAre", Res.get("shared.buying"), baseCurrencyCode, Res.get("shared.selling"), currencyCode); } public static String getOfferDirectionForCreateOffer(OfferDirection direction, String currencyCode, boolean isPrivate) { String baseCurrencyCode = Res.getBaseCurrencyCode(); - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - return direction == OfferDirection.BUY ? - Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), baseCurrencyCode); - } else { - return direction == OfferDirection.SELL ? - Res.get("formatter.youAreCreatingAnOffer.crypto" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), currencyCode, Res.get("shared.selling"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.crypto" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), currencyCode, Res.get("shared.buying"), baseCurrencyCode); - } + return direction == OfferDirection.BUY ? + Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.buy"), baseCurrencyCode) : + Res.get("formatter.youAreCreatingAnOffer.traditional" + (isPrivate ? LOCKED : ""), Res.get("shared.sell"), baseCurrencyCode); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index ae3e7ed266..dbee100f06 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -135,6 +135,24 @@ public class FormBuilder { return titledGroupBg; } + /////////////////////////////////////////////////////////////////////////////////////////// + // Divider + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Region addSeparator(GridPane gridPane, int rowIndex) { + Region separator = new Region(); + separator.getStyleClass().add("grid-pane-separator"); + separator.setPrefHeight(1); + separator.setMinHeight(1); + separator.setMaxHeight(1); + GridPane.setRowIndex(separator, rowIndex); + GridPane.setColumnIndex(separator, 0); + GridPane.setColumnSpan(separator, 2); + gridPane.getChildren().add(separator); + separator.setPrefHeight(1); + GridPane.setMargin(separator, new Insets(0, 0, 3, 0)); + return separator; + } /////////////////////////////////////////////////////////////////////////////////////////// // Label @@ -361,7 +379,7 @@ public class FormBuilder { textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); Button button = new AutoTooltipButton("..."); - button.setStyle("-fx-min-width: 26; -fx-pref-height: 26; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); + button.setStyle("-fx-min-width: 32; -fx-padding: 0 0 10 0; -fx-background-color: -fx-background;"); button.managedProperty().bind(button.visibleProperty()); HBox hbox = new HBox(textField, button); @@ -369,6 +387,7 @@ public class FormBuilder { hbox.setSpacing(8); VBox vbox = getTopLabelVBox(0); + vbox.setSpacing(2); vbox.getChildren().addAll(getTopLabel(title), hbox); gridPane.getChildren().add(vbox); @@ -490,6 +509,7 @@ public class FormBuilder { GridPane.setColumnIndex(textArea, 1); GridPane.setMargin(label, new Insets(top, 0, 0, 0)); GridPane.setHalignment(label, HPos.LEFT); + GridPane.setValignment(label, VPos.TOP); GridPane.setMargin(textArea, new Insets(top, 0, 0, 0)); return new Tuple2<>(label, textArea); @@ -617,6 +637,7 @@ public class FormBuilder { JFXTextArea textArea = new HavenoTextArea(); textArea.setPromptText(prompt); textArea.setLabelFloat(true); + textArea.getStyleClass().add("label-float"); textArea.setWrapText(true); GridPane.setRowIndex(textArea, rowIndex); @@ -805,9 +826,9 @@ public class FormBuilder { } public static InputTextField addInputTextField(GridPane gridPane, int rowIndex, String title, double top) { - InputTextField inputTextField = new InputTextField(); inputTextField.setLabelFloat(true); + inputTextField.getStyleClass().add("label-float"); inputTextField.setPromptText(title); GridPane.setRowIndex(inputTextField, rowIndex); GridPane.setColumnIndex(inputTextField, 0); @@ -884,6 +905,8 @@ public class FormBuilder { public static PasswordTextField addPasswordTextField(GridPane gridPane, int rowIndex, String title, double top) { PasswordTextField passwordField = new PasswordTextField(); + passwordField.getStyleClass().addAll("label-float"); + GUIUtil.applyFilledStyle(passwordField); passwordField.setPromptText(title); GridPane.setRowIndex(passwordField, rowIndex); GridPane.setColumnIndex(passwordField, 0); @@ -1006,8 +1029,10 @@ public class FormBuilder { InputTextField inputTextField1 = new InputTextField(); inputTextField1.setPromptText(title1); inputTextField1.setLabelFloat(true); + inputTextField1.getStyleClass().add("label-float"); InputTextField inputTextField2 = new InputTextField(); inputTextField2.setLabelFloat(true); + inputTextField2.getStyleClass().add("label-float"); inputTextField2.setPromptText(title2); HBox hBox = new HBox(); @@ -1228,6 +1253,7 @@ public class FormBuilder { public static ComboBox addComboBox(GridPane gridPane, int rowIndex, int top) { final JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); GridPane.setRowIndex(comboBox, rowIndex); GridPane.setMargin(comboBox, new Insets(top, 0, 0, 0)); @@ -1264,7 +1290,9 @@ public class FormBuilder { VBox vBox = getTopLabelVBox(top); final JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(prompt); + comboBox.setPadding(new Insets(top, 0, 0, 12)); vBox.getChildren().addAll(label, comboBox); @@ -1389,7 +1417,9 @@ public class FormBuilder { public static ComboBox addComboBox(GridPane gridPane, int rowIndex, String title, double top) { JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); @@ -1399,6 +1429,7 @@ public class FormBuilder { GridPane.setRowIndex(comboBox, rowIndex); GridPane.setColumnIndex(comboBox, 0); + comboBox.setPadding(new Insets(0, 0, 0, 12)); GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(comboBox); @@ -1407,7 +1438,9 @@ public class FormBuilder { public static AutocompleteComboBox addAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) { var comboBox = new AutocompleteComboBox(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); comboBox.setPromptText(title); comboBox.setMaxWidth(Double.MAX_VALUE); @@ -1469,6 +1502,7 @@ public class FormBuilder { AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); topLabelVBox2.getChildren().addAll(topLabel2, comboBox); hBox.getChildren().addAll(topLabelVBox1, topLabelVBox2); @@ -1498,7 +1532,9 @@ public class FormBuilder { hBox.setSpacing(10); ComboBox comboBox1 = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox1); ComboBox comboBox2 = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox2); hBox.getChildren().addAll(comboBox1, comboBox2); final Tuple2 topLabelWithVBox = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); @@ -1526,8 +1562,10 @@ public class FormBuilder { hBox.setSpacing(10); JFXComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); comboBox.setPromptText(titleCombobox); comboBox.setLabelFloat(true); + comboBox.getStyleClass().add("label-float"); TextField textField = new HavenoTextField(); @@ -1570,6 +1608,7 @@ public class FormBuilder { button.setDefaultButton(true); ComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); hBox.getChildren().addAll(comboBox, button); @@ -1604,6 +1643,7 @@ public class FormBuilder { hBox.setSpacing(10); ComboBox comboBox = new JFXComboBox<>(); + GUIUtil.applyFilledStyle(comboBox); TextField textField = new TextField(textFieldText); textField.setEditable(false); textField.setMouseTransparent(true); @@ -1797,6 +1837,7 @@ public class FormBuilder { return new Tuple2<>(label, textFieldWithCopyIcon); } + /////////////////////////////////////////////////////////////////////////////////////////// // Label + AddressTextField /////////////////////////////////////////////////////////////////////////////////////////// @@ -2181,11 +2222,13 @@ public class FormBuilder { Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); + HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(input, Priority.ALWAYS); input.setMaxWidth(Double.MAX_VALUE); - box.getStyleClass().add("input-with-border"); + box.setAlignment(Pos.CENTER_LEFT); + box.getStyleClass().add("offer-input"); box.getChildren().addAll(input, label); return new Tuple3<>(box, input, label); } @@ -2197,11 +2240,13 @@ public class FormBuilder { Label label = new AutoTooltipLabel(Res.getBaseCurrencyCode()); label.getStyleClass().add("input-label"); + HBox.setMargin(label, new Insets(0, 8, 0, 0)); HBox box = new HBox(); HBox.setHgrow(infoInputTextField, Priority.ALWAYS); infoInputTextField.setMaxWidth(Double.MAX_VALUE); - box.getStyleClass().add("input-with-border"); + box.setAlignment(Pos.CENTER_LEFT); + box.getStyleClass().add("offer-input"); box.getChildren().addAll(infoInputTextField, label); return new Tuple3<>(box, infoInputTextField, label); } @@ -2444,6 +2489,7 @@ public class FormBuilder { if (groupStyle != null) titledGroupBg.getStyleClass().add(groupStyle); TableView tableView = new TableView<>(); + GUIUtil.applyTableStyle(tableView); GridPane.setRowIndex(tableView, rowIndex); GridPane.setMargin(tableView, new Insets(top + 30, -10, 5, -10)); gridPane.getChildren().add(tableView); diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index e825e29e6e..cb8111af33 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -25,7 +25,10 @@ import com.googlecode.jcsv.CSVStrategy; import com.googlecode.jcsv.writer.CSVEntryConverter; import com.googlecode.jcsv.writer.CSVWriter; import com.googlecode.jcsv.writer.internal.CSVWriterBuilder; + +import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.CorruptedStorageFileHandler; @@ -64,9 +67,17 @@ import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.traditionalaccounts.TraditionalAccountsView; import haveno.desktop.main.overlays.popups.Popup; import haveno.network.p2p.P2PService; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.geometry.HPos; +import javafx.geometry.Insets; import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.ComboBox; @@ -76,14 +87,21 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Modality; @@ -106,6 +124,8 @@ import java.io.OutputStreamWriter; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; @@ -131,7 +151,9 @@ public class GUIUtil { public final static int NUM_DECIMALS_PRECISE = 7; public final static int AMOUNT_DECIMALS_WITH_ZEROS = 3; public final static int AMOUNT_DECIMALS = 4; + public static final double NUM_OFFERS_TRANSLATE_X = -13.0; + public static final boolean disablePaymentUriLabel = true; // universally disable payment uri labels, allowing bigger xmr logo overlays private static Preferences preferences; public static void setPreferences(Preferences preferences) { @@ -304,30 +326,42 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label currencyType = new AutoTooltipLabel( - CurrencyUtil.isTraditionalCurrency(code) ? Res.get("shared.traditional") : Res.get("shared.crypto")); + box.setAlignment(Pos.CENTER_LEFT); + Label label1 = new AutoTooltipLabel(getCurrencyType(code)); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.tradeCurrency.getNameAndCode() : code); + label2.getStyleClass().add("currency-label"); + Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.tradeCurrency.getName()); + if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); + Label label4 = new AutoTooltipLabel(); - currencyType.getStyleClass().add("currency-label-small"); - Label currency = new AutoTooltipLabel(code); - currency.getStyleClass().add("currency-label"); - Label offers = new AutoTooltipLabel(item.tradeCurrency.getName()); - offers.getStyleClass().add("currency-label"); - - box.getChildren().addAll(currencyType, currency, offers); + box.getChildren().addAll(label1, label2, label3); + if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); switch (code) { case GUIUtil.SHOW_ALL_FLAG: - currencyType.setText(Res.get("shared.all")); - currency.setText(Res.get("list.currency.showAll")); + label1.setText(Res.get("shared.all")); + label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: - currencyType.setText(Res.get("shared.edit")); - currency.setText(Res.get("list.currency.editList")); + label1.setText(Res.get("shared.edit")); + label2.setText(Res.get("list.currency.editList")); break; default: - if (preferences.isSortMarketCurrenciesNumerically()) { - offers.setText(offers.getText() + " (" + item.numTrades + " " + - (item.numTrades == 1 ? postFixSingle : postFixMulti) + ")"); + + // use icon if available + StackPane currencyIcon = getCurrencyIcon(code); + if (currencyIcon != null) { + label1.setText(""); + label1.setGraphic(currencyIcon); + } + + if (preferences.isSortMarketCurrenciesNumerically() && item.numTrades > 0) { + boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); + Label offersTarget = isCrypto ? label3 : label4; + HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); + offersTarget.getStyleClass().add("offer-label"); + offersTarget.setText(item.numTrades + " " + (item.numTrades == 1 ? postFixSingle : postFixMulti)); } } @@ -353,7 +387,7 @@ public class GUIUtil { String code = item.getCode(); AnchorPane pane = new AnchorPane(); - Label currency = new AutoTooltipLabel(code + " - " + item.getName()); + Label currency = new AutoTooltipLabel(item.getName() + " (" + item.getCode() + ")"); currency.getStyleClass().add("currency-label-selected"); AnchorPane.setLeftAnchor(currency, 0.0); pane.getChildren().add(currency); @@ -388,34 +422,6 @@ public class GUIUtil { }; } - public static StringConverter getTradeCurrencyConverter(String postFixSingle, - String postFixMulti, - Map offerCounts) { - return new StringConverter<>() { - @Override - public String toString(TradeCurrency tradeCurrency) { - String code = tradeCurrency.getCode(); - Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); - final String displayString; - displayString = offerCountOptional - .map(offerCount -> CurrencyUtil.getNameAndCode(code) - + " - " + offerCount + " " + (offerCount == 1 ? postFixSingle : postFixMulti)) - .orElseGet(() -> CurrencyUtil.getNameAndCode(code)); - // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 - if (code.equals(GUIUtil.SHOW_ALL_FLAG)) - return "▶ " + Res.get("list.currency.showAll"); - else if (code.equals(GUIUtil.EDIT_FLAG)) - return "▼ " + Res.get("list.currency.editList"); - return tradeCurrency.getDisplayPrefix() + displayString; - } - - @Override - public TradeCurrency fromString(String s) { - return null; - } - }; - } - public static Callback, ListCell> getTradeCurrencyCellFactory(String postFixSingle, String postFixMulti, Map offerCounts) { @@ -430,33 +436,48 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label currencyType = new AutoTooltipLabel( - CurrencyUtil.isTraditionalCurrency(item.getCode()) ? Res.get("shared.traditional") : Res.get("shared.crypto")); + box.setAlignment(Pos.CENTER_LEFT); - currencyType.getStyleClass().add("currency-label-small"); - Label currency = new AutoTooltipLabel(item.getCode()); - currency.getStyleClass().add("currency-label"); - Label offers = new AutoTooltipLabel(item.getName()); - offers.getStyleClass().add("currency-label"); - - box.getChildren().addAll(currencyType, currency, offers); + Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? item.getNameAndCode() : code); + label2.getStyleClass().add("currency-label"); + Label label3 = new AutoTooltipLabel(CurrencyUtil.isCryptoCurrency(code) ? "" : item.getName()); + if (!CurrencyUtil.isCryptoCurrency(code)) label3.getStyleClass().add("currency-label"); + Label label4 = new AutoTooltipLabel(); Optional offerCountOptional = Optional.ofNullable(offerCounts.get(code)); switch (code) { case GUIUtil.SHOW_ALL_FLAG: - currencyType.setText(Res.get("shared.all")); - currency.setText(Res.get("list.currency.showAll")); + label1.setText(Res.get("shared.all")); + label2.setText(Res.get("list.currency.showAll")); break; case GUIUtil.EDIT_FLAG: - currencyType.setText(Res.get("shared.edit")); - currency.setText(Res.get("list.currency.editList")); + label1.setText(Res.get("shared.edit")); + label2.setText(Res.get("list.currency.editList")); break; default: - offerCountOptional.ifPresent(numOffer -> offers.setText(offers.getText() + " (" + numOffer + " " + - (numOffer == 1 ? postFixSingle : postFixMulti) + ")")); + + // use icon if available + StackPane currencyIcon = getCurrencyIcon(code); + if (currencyIcon != null) { + label1.setText(""); + label1.setGraphic(currencyIcon); + } + + boolean isCrypto = CurrencyUtil.isCryptoCurrency(code); + Label offersTarget = isCrypto ? label3 : label4; + offerCountOptional.ifPresent(numOffers -> { + HBox.setMargin(offersTarget, new Insets(0, 0, 0, NUM_OFFERS_TRANSLATE_X)); + offersTarget.getStyleClass().add("offer-label"); + offersTarget.setText(numOffers + " " + (numOffers == 1 ? postFixSingle : postFixMulti)); + }); } + box.getChildren().addAll(label1, label2, label3); + if (!CurrencyUtil.isCryptoCurrency(code)) box.getChildren().add(label4); + setGraphic(box); } else { @@ -466,6 +487,56 @@ public class GUIUtil { }; } + public static Callback, ListCell> getTradeCurrencyCellFactoryNameAndCode() { + return p -> new ListCell<>() { + @Override + protected void updateItem(TradeCurrency item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + + HBox box = new HBox(); + box.setSpacing(10); + + Label label1 = new AutoTooltipLabel(getCurrencyType(item.getCode())); + label1.getStyleClass().add("currency-label-small"); + Label label2 = new AutoTooltipLabel(item.getNameAndCode()); + label2.getStyleClass().add("currency-label"); + + // use icon if available + StackPane currencyIcon = getCurrencyIcon(item.getCode()); + if (currencyIcon != null) { + label1.setText(""); + label1.setGraphic(currencyIcon); + } + + box.getChildren().addAll(label1, label2); + + setGraphic(box); + + } else { + setGraphic(null); + } + } + }; + } + + private static String getCurrencyType(String code) { + if (CurrencyUtil.isFiatCurrency(code)) { + return Res.get("shared.fiat"); + } else if (CurrencyUtil.isTraditionalCurrency(code)) { + return Res.get("shared.traditional"); + } else if (CurrencyUtil.isCryptoCurrency(code)) { + return Res.get("shared.crypto"); + } else { + return ""; + } + } + + private static String getCurrencyType(PaymentMethod method) { + return method.isTraditional() ? Res.get("shared.traditional") : Res.get("shared.crypto"); + } + public static ListCell getPaymentMethodButtonCell() { return new ListCell<>() { @@ -501,9 +572,7 @@ public class GUIUtil { HBox box = new HBox(); box.setSpacing(20); - Label paymentType = new AutoTooltipLabel( - method.isTraditional() ? Res.get("shared.traditional") : Res.get("shared.crypto")); - + Label paymentType = new AutoTooltipLabel(getCurrencyType(method)); paymentType.getStyleClass().add("currency-label-small"); Label paymentMethod = new AutoTooltipLabel(Res.get(id)); paymentMethod.getStyleClass().add("currency-label"); @@ -628,13 +697,26 @@ public class GUIUtil { private static void doOpenWebPage(String target) { try { - Utilities.openURI(new URI(target)); + Utilities.openURI(safeParse(target)); } catch (Exception e) { e.printStackTrace(); log.error(e.getMessage()); } } + private static URI safeParse(String url) throws URISyntaxException { + int hashIndex = url.indexOf('#'); + + if (hashIndex >= 0 && hashIndex < url.length() - 1) { + String base = url.substring(0, hashIndex); + String fragment = url.substring(hashIndex + 1); + String encodedFragment = URLEncoder.encode(fragment, StandardCharsets.UTF_8); + return new URI(base + "#" + encodedFragment); + } + + return new URI(url); // no fragment + } + public static String getPercentageOfTradeAmount(BigInteger fee, BigInteger tradeAmount) { String result = " (" + getPercentage(fee, tradeAmount) + " " + Res.get("guiUtil.ofTradeAmount") + ")"; @@ -673,7 +755,7 @@ public class GUIUtil { String currencyName = Config.baseCurrencyNetwork().getCurrencyName(); new Popup().information(Res.get("payment.fasterPayments.newRequirements.info", currencyName)) .width(900) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -683,10 +765,10 @@ public class GUIUtil { } public static String getMoneroURI(String address, BigInteger amount, String label) { - return MoneroUtils.getPaymentUri(new MoneroTxConfig() - .setAddress(address) - .setAmount(amount) - .setNote(label)); + MoneroTxConfig txConfig = new MoneroTxConfig().setAddress(address); + if (amount != null) txConfig.setAmount(amount); + if (label != null && !label.isEmpty() && !disablePaymentUriLabel) txConfig.setNote(label); + return MoneroUtils.getPaymentUri(txConfig); } public static boolean isBootstrappedOrShowPopup(P2PService p2PService) { @@ -742,7 +824,7 @@ public class GUIUtil { if (user.currentPaymentAccountProperty().get() == null) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) - .actionButtonTextWithGoTo("navigation.account") + .actionButtonTextWithGoTo("mainView.menu.account") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); navigation.navigateTo(MainView.class, AccountView.class, TraditionalAccountsView.class); @@ -1033,4 +1115,290 @@ public class GUIUtil { columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } + + public static void applyFilledStyle(TextField textField) { + textField.textProperty().addListener((observable, oldValue, newValue) -> { + updateFilledStyle(textField); + }); + } + + private static void updateFilledStyle(TextField textField) { + if (textField.getText() != null && !textField.getText().isEmpty()) { + if (!textField.getStyleClass().contains("filled")) { + textField.getStyleClass().add("filled"); + } + } else { + textField.getStyleClass().remove("filled"); + } + } + + public static void applyFilledStyle(ComboBox comboBox) { + comboBox.valueProperty().addListener((observable, oldValue, newValue) -> { + updateFilledStyle(comboBox); + }); + } + + private static void updateFilledStyle(ComboBox comboBox) { + if (comboBox.getValue() != null) { + if (!comboBox.getStyleClass().contains("filled")) { + comboBox.getStyleClass().add("filled"); + } + } else { + comboBox.getStyleClass().remove("filled"); + } + } + + public static void applyTableStyle(TableView tableView) { + applyTableStyle(tableView, true); + } + + public static void applyTableStyle(TableView tableView, boolean applyRoundedArc) { + if (applyRoundedArc) applyRoundedArc(tableView); + addSpacerColumns(tableView); + applyEdgeColumnStyleClasses(tableView); + } + + private static void applyRoundedArc(TableView tableView) { + Rectangle clip = new Rectangle(); + clip.setArcWidth(Layout.ROUNDED_ARC); + clip.setArcHeight(Layout.ROUNDED_ARC); + tableView.setClip(clip); + tableView.layoutBoundsProperty().addListener((obs, oldVal, newVal) -> { + clip.setWidth(newVal.getWidth()); + clip.setHeight(newVal.getHeight()); + }); + } + + private static void addSpacerColumns(TableView tableView) { + TableColumn leftSpacer = new TableColumn<>(); + TableColumn rightSpacer = new TableColumn<>(); + + configureSpacerColumn(leftSpacer); + configureSpacerColumn(rightSpacer); + + tableView.getColumns().add(0, leftSpacer); + tableView.getColumns().add(rightSpacer); + } + + private static void configureSpacerColumn(TableColumn column) { + column.setPrefWidth(15); + column.setMaxWidth(15); + column.setMinWidth(15); + column.setReorderable(false); + column.setResizable(false); + column.setSortable(false); + column.setCellFactory(col -> new TableCell<>()); // empty cell + } + + private static void applyEdgeColumnStyleClasses(TableView tableView) { + ListChangeListener> columnListener = change -> { + UserThread.execute(() -> { + updateEdgeColumnStyleClasses(tableView); + }); + }; + + tableView.getColumns().addListener(columnListener); + tableView.skinProperty().addListener((obs, oldSkin, newSkin) -> { + if (newSkin != null) { + UserThread.execute(() -> { + updateEdgeColumnStyleClasses(tableView); + }); + } + }); + + // react to size changes + ChangeListener sizeListener = (obs, oldVal, newVal) -> updateEdgeColumnStyleClasses(tableView); + tableView.heightProperty().addListener(sizeListener); + tableView.widthProperty().addListener(sizeListener); + + updateEdgeColumnStyleClasses(tableView); + } + + private static void updateEdgeColumnStyleClasses(TableView tableView) { + ObservableList> columns = tableView.getColumns(); + + // find columns with "first-column" and "last-column" classes + TableColumn firstCol = null; + TableColumn lastCol = null; + for (TableColumn col : columns) { + if (col.getStyleClass().contains("first-column")) { + firstCol = col; + } else if (col.getStyleClass().contains("last-column")) { + lastCol = col; + } + } + + // handle if columns do not exist + if (firstCol == null || lastCol == null) { + if (firstCol != null) throw new IllegalStateException("Missing column with 'last-column'"); + if (lastCol != null) throw new IllegalStateException("Missing column with 'first-column'"); + + // remove all classes + for (TableColumn col : columns) { + col.getStyleClass().removeAll("first-column", "last-column"); + } + + // apply first and last classes + if (!columns.isEmpty()) { + TableColumn first = columns.get(0); + TableColumn last = columns.get(columns.size() - 1); + + if (!first.getStyleClass().contains("first-column")) { + first.getStyleClass().add("first-column"); + } + + if (!last.getStyleClass().contains("last-column")) { + last.getStyleClass().add("last-column"); + } + } + } else { + + // done if correct order + if (columns.get(0) == firstCol && columns.get(columns.size() - 1) == lastCol) { + return; + } + + // set first and last columns + if (columns.get(0) != firstCol) { + columns.remove(firstCol); + columns.add(0, firstCol); + } + if (columns.get(columns.size() - 1) != lastCol) { + columns.remove(lastCol); + columns.add(firstCol == lastCol ? columns.size() - 1 : columns.size(), lastCol); + } + } + } + + public static ObservableList> getContentColumns(TableView tableView) { + ObservableList> contentColumns = FXCollections.observableArrayList(); + for (TableColumn column : tableView.getColumns()) { + if (!column.getStyleClass().contains("first-column") && !column.getStyleClass().contains("last-column")) { + contentColumns.add(column); + } + } + return contentColumns; + } + + private static ImageView getCurrencyImageView(String currencyCode) { + return getCurrencyImageView(currencyCode, 24); + } + + private static ImageView getCurrencyImageView(String currencyCode, double size) { + if (currencyCode == null) return null; + String imageId = getImageId(currencyCode); + if (imageId == null) return null; + ImageView icon = new ImageView(); + icon.setFitWidth(size); + icon.setPreserveRatio(true); + icon.setSmooth(true); + icon.setCache(true); + icon.setId(imageId); + return icon; + } + + public static StackPane getCurrencyIcon(String currencyCode) { + ImageView icon = getCurrencyImageView(currencyCode); + return icon == null ? null : new StackPane(icon); + } + + public static StackPane getCurrencyIcon(String currencyCode, double size) { + ImageView icon = getCurrencyImageView(currencyCode, size); + return icon == null ? null : new StackPane(icon); + } + + public static StackPane getCurrencyIconWithBorder(String currencyCode) { + return getCurrencyIconWithBorder(currencyCode, 25, 1); + } + + public static StackPane getCurrencyIconWithBorder(String currencyCode, double size, double borderWidth) { + if (currencyCode == null) return null; + + ImageView icon = getCurrencyImageView(currencyCode, size); + icon.setFitWidth(size - 2 * borderWidth); + icon.setFitHeight(size - 2 * borderWidth); + + StackPane circleWrapper = new StackPane(icon); + circleWrapper.setPrefSize(size, size); + circleWrapper.setMaxSize(size, size); + circleWrapper.setMinSize(size, size); + + circleWrapper.setStyle( + "-fx-background-color: white;" + + "-fx-background-radius: 50%;" + + "-fx-border-radius: 50%;" + + "-fx-border-color: white;" + + "-fx-border-width: " + borderWidth + "px;" + ); + + StackPane.setAlignment(icon, Pos.CENTER); + + return circleWrapper; + } + + private static String getImageId(String currencyCode) { + if (currencyCode == null) return null; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) return "image-" + currencyCode.toLowerCase() + "-logo"; + if (CurrencyUtil.isFiatCurrency(currencyCode)) return "image-fiat-logo"; + return null; + } + + public static void adjustHeightAutomatically(TextArea textArea) { + adjustHeightAutomatically(textArea, null); + } + + public static void adjustHeightAutomatically(TextArea textArea, Double maxHeight) { + textArea.sceneProperty().addListener((o, oldScene, newScene) -> { + if (newScene != null) { + // avoid javafx css warning + CssTheme.loadSceneStyles(newScene, CssTheme.getCurrentTheme(), false); + textArea.applyCss(); + var text = textArea.lookup(".text"); + + textArea.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> { + Insets padding = textArea.getInsets(); + double topBottomPadding = padding.getTop() + padding.getBottom(); + double prefHeight = textArea.getFont().getSize() + text.getBoundsInLocal().getHeight() + topBottomPadding; + return maxHeight == null ? prefHeight : Math.min(prefHeight, maxHeight); + }, text.boundsInLocalProperty())); + + text.boundsInLocalProperty().addListener((observableBoundsAfter, boundsBefore, boundsAfter) -> { + Platform.runLater(() -> textArea.requestLayout()); + }); + } + }); + } + + public static Label getLockLabel() { + Label lockLabel = FormBuilder.getIcon(AwesomeIcon.LOCK, "16px"); + lockLabel.setStyle(lockLabel.getStyle() + " -fx-text-fill: white;"); + return lockLabel; + } + + public static MaterialDesignIconView getCopyIcon() { + return new MaterialDesignIconView(MaterialDesignIcon.CONTENT_COPY, "1.35em"); + } + + + public static Tuple2 getSmallXmrQrCodePane() { + return getXmrQrCodePane(150, disablePaymentUriLabel ? 32 : 28, 2); + } + + public static Tuple2 getBigXmrQrCodePane() { + return getXmrQrCodePane(250, disablePaymentUriLabel ? 47 : 45, 3); + } + + private static Tuple2 getXmrQrCodePane(int qrCodeSize, int logoSize, int logoBorderWidth) { + ImageView qrCodeImageView = new ImageView(); + qrCodeImageView.setFitHeight(qrCodeSize); + qrCodeImageView.setFitWidth(qrCodeSize); + qrCodeImageView.getStyleClass().add("qr-code"); + + StackPane xmrLogo = GUIUtil.getCurrencyIconWithBorder(Res.getBaseCurrencyCode(), logoSize, logoBorderWidth); + StackPane qrCodePane = new StackPane(qrCodeImageView, xmrLogo); + qrCodePane.setCursor(Cursor.HAND); + qrCodePane.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + + return new Tuple2<>(qrCodePane, qrCodeImageView); + } } diff --git a/desktop/src/main/java/haveno/desktop/util/Layout.java b/desktop/src/main/java/haveno/desktop/util/Layout.java index 975bb40df6..442a60db83 100644 --- a/desktop/src/main/java/haveno/desktop/util/Layout.java +++ b/desktop/src/main/java/haveno/desktop/util/Layout.java @@ -22,10 +22,11 @@ public class Layout { public static final double INITIAL_WINDOW_HEIGHT = 710; //740 public static final double MIN_WINDOW_WIDTH = 1020; public static final double MIN_WINDOW_HEIGHT = 620; + public static final double MAX_POPUP_HEIGHT = 730; public static final double FIRST_ROW_DISTANCE = 20d; public static final double COMPACT_FIRST_ROW_DISTANCE = 10d; public static final double TWICE_FIRST_ROW_DISTANCE = 20d * 2; - public static final double FLOATING_LABEL_DISTANCE = 18d; + public static final double FLOATING_LABEL_DISTANCE = 23d; public static final double GROUP_DISTANCE = 40d; public static final double COMPACT_GROUP_DISTANCE = 30d; public static final double GROUP_DISTANCE_WITHOUT_SEPARATOR = 20d; @@ -33,6 +34,7 @@ public class Layout { public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + COMPACT_FIRST_ROW_DISTANCE; public static final double COMPACT_FIRST_ROW_AND_GROUP_DISTANCE_WITHOUT_SEPARATOR = GROUP_DISTANCE_WITHOUT_SEPARATOR + COMPACT_FIRST_ROW_DISTANCE; + public static final double TWICE_FIRST_ROW_AND_COMPACT_GROUP_DISTANCE = COMPACT_GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double TWICE_FIRST_ROW_AND_GROUP_DISTANCE = GROUP_DISTANCE + TWICE_FIRST_ROW_DISTANCE; public static final double PADDING_WINDOW = 20d; public static double PADDING = 10d; @@ -40,4 +42,8 @@ public class Layout { public static final double SPACING_V_BOX = 5d; public static final double GRID_GAP = 5d; public static final double LIST_ROW_HEIGHT = 34; + public static final double ROUNDED_ARC = 20; + public static final double FLOATING_ICON_Y = 9; // adjust when .jfx-text-field padding is changed for right icons + public static final double DETAILS_WINDOW_WIDTH = 950; + public static final double DETAILS_WINDOW_EXTRA_INFO_MAX_HEIGHT = 150; } diff --git a/desktop/src/main/java/haveno/desktop/util/Transitions.java b/desktop/src/main/java/haveno/desktop/util/Transitions.java index 300524ccce..691d9d6243 100644 --- a/desktop/src/main/java/haveno/desktop/util/Transitions.java +++ b/desktop/src/main/java/haveno/desktop/util/Transitions.java @@ -37,7 +37,7 @@ import javafx.util.Duration; @Singleton public class Transitions { - public final static int DEFAULT_DURATION = 600; + public final static int DEFAULT_DURATION = 400; private final Preferences preferences; private Timeline removeEffectTimeLine; @@ -96,7 +96,7 @@ public class Transitions { // Blur public void blur(Node node) { - blur(node, DEFAULT_DURATION, -0.1, false, 15); + blur(node, DEFAULT_DURATION, -0.1, false, 45); } public void blur(Node node, int duration, double brightness, boolean removeNode, double blurRadius) { @@ -111,7 +111,7 @@ public class Transitions { ColorAdjust darken = new ColorAdjust(); darken.setBrightness(0.0); blur.setInput(darken); - KeyValue kv2 = new KeyValue(darken.brightnessProperty(), brightness); + KeyValue kv2 = new KeyValue(darken.brightnessProperty(), CssTheme.isDarkTheme() ? brightness * -0.13 : brightness); KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration(duration)), kv2); timeline.getKeyFrames().addAll(kf1, kf2); node.setEffect(blur); diff --git a/desktop/src/main/resources/images/account.png b/desktop/src/main/resources/images/account.png new file mode 100644 index 0000000000..53b4058465 Binary files /dev/null and b/desktop/src/main/resources/images/account.png differ diff --git a/desktop/src/main/resources/images/ada_logo.png b/desktop/src/main/resources/images/ada_logo.png new file mode 100644 index 0000000000..70d1166426 Binary files /dev/null and b/desktop/src/main/resources/images/ada_logo.png differ diff --git a/desktop/src/main/resources/images/alert_round.png b/desktop/src/main/resources/images/alert_round.png index c5c182ed94..4ba0578457 100644 Binary files a/desktop/src/main/resources/images/alert_round.png and b/desktop/src/main/resources/images/alert_round.png differ diff --git a/desktop/src/main/resources/images/bch_logo.png b/desktop/src/main/resources/images/bch_logo.png new file mode 100644 index 0000000000..3c30cfb9fb Binary files /dev/null and b/desktop/src/main/resources/images/bch_logo.png differ diff --git a/desktop/src/main/resources/images/blue_circle.png b/desktop/src/main/resources/images/blue_circle_solid.png similarity index 100% rename from desktop/src/main/resources/images/blue_circle.png rename to desktop/src/main/resources/images/blue_circle_solid.png diff --git a/desktop/src/main/resources/images/blue_circle@2x.png b/desktop/src/main/resources/images/blue_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/blue_circle@2x.png rename to desktop/src/main/resources/images/blue_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/btc_logo.png b/desktop/src/main/resources/images/btc_logo.png new file mode 100644 index 0000000000..d3c3e7a7c8 Binary files /dev/null and b/desktop/src/main/resources/images/btc_logo.png differ diff --git a/desktop/src/main/resources/images/connection/tor.png b/desktop/src/main/resources/images/connection/tor.png index a88b4310cb..6553e06dac 100644 Binary files a/desktop/src/main/resources/images/connection/tor.png and b/desktop/src/main/resources/images/connection/tor.png differ diff --git a/desktop/src/main/resources/images/connection/tor@2x.png b/desktop/src/main/resources/images/connection/tor@2x.png deleted file mode 100644 index d8528c0c3c..0000000000 Binary files a/desktop/src/main/resources/images/connection/tor@2x.png and /dev/null differ diff --git a/desktop/src/main/resources/images/connection/tor_color@2x.png b/desktop/src/main/resources/images/connection/tor_color@2x.png deleted file mode 100644 index 1f2924adc0..0000000000 Binary files a/desktop/src/main/resources/images/connection/tor_color@2x.png and /dev/null differ diff --git a/desktop/src/main/resources/images/dai-erc20_logo.png b/desktop/src/main/resources/images/dai-erc20_logo.png new file mode 100644 index 0000000000..698ffc48eb Binary files /dev/null and b/desktop/src/main/resources/images/dai-erc20_logo.png differ diff --git a/desktop/src/main/resources/images/dark_mode_toggle.png b/desktop/src/main/resources/images/dark_mode_toggle.png new file mode 100644 index 0000000000..ceeede409d Binary files /dev/null and b/desktop/src/main/resources/images/dark_mode_toggle.png differ diff --git a/desktop/src/main/resources/images/doge_logo.png b/desktop/src/main/resources/images/doge_logo.png new file mode 100644 index 0000000000..041d833fca Binary files /dev/null and b/desktop/src/main/resources/images/doge_logo.png differ diff --git a/desktop/src/main/resources/images/eth_logo.png b/desktop/src/main/resources/images/eth_logo.png new file mode 100644 index 0000000000..17dd1a92a5 Binary files /dev/null and b/desktop/src/main/resources/images/eth_logo.png differ diff --git a/desktop/src/main/resources/images/fiat_logo_dark_mode.png b/desktop/src/main/resources/images/fiat_logo_dark_mode.png new file mode 100644 index 0000000000..b6fbaf02f3 Binary files /dev/null and b/desktop/src/main/resources/images/fiat_logo_dark_mode.png differ diff --git a/desktop/src/main/resources/images/fiat_logo_light_mode.png b/desktop/src/main/resources/images/fiat_logo_light_mode.png new file mode 100644 index 0000000000..a40b3e632d Binary files /dev/null and b/desktop/src/main/resources/images/fiat_logo_light_mode.png differ diff --git a/desktop/src/main/resources/images/green_circle.png b/desktop/src/main/resources/images/green_circle.png index 1555f43f0a..439ae2571b 100644 Binary files a/desktop/src/main/resources/images/green_circle.png and b/desktop/src/main/resources/images/green_circle.png differ diff --git a/desktop/src/main/resources/images/green_circle_solid.png b/desktop/src/main/resources/images/green_circle_solid.png new file mode 100644 index 0000000000..1555f43f0a Binary files /dev/null and b/desktop/src/main/resources/images/green_circle_solid.png differ diff --git a/desktop/src/main/resources/images/green_circle@2x.png b/desktop/src/main/resources/images/green_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/green_circle@2x.png rename to desktop/src/main/resources/images/green_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/light_mode_toggle.png b/desktop/src/main/resources/images/light_mode_toggle.png new file mode 100644 index 0000000000..870c5c209e Binary files /dev/null and b/desktop/src/main/resources/images/light_mode_toggle.png differ diff --git a/desktop/src/main/resources/images/lock@2x.png b/desktop/src/main/resources/images/lock@2x.png index 371f6aeb5d..f176e08203 100644 Binary files a/desktop/src/main/resources/images/lock@2x.png and b/desktop/src/main/resources/images/lock@2x.png differ diff --git a/desktop/src/main/resources/images/lock_circle.png b/desktop/src/main/resources/images/lock_circle.png new file mode 100644 index 0000000000..f176e08203 Binary files /dev/null and b/desktop/src/main/resources/images/lock_circle.png differ diff --git a/desktop/src/main/resources/images/logo_landscape_dark_mode.png b/desktop/src/main/resources/images/logo_landscape_dark_mode.png new file mode 100644 index 0000000000..ce3ffd6747 Binary files /dev/null and b/desktop/src/main/resources/images/logo_landscape_dark_mode.png differ diff --git a/desktop/src/main/resources/images/logo_landscape_light_mode.png b/desktop/src/main/resources/images/logo_landscape_light_mode.png new file mode 100644 index 0000000000..e7ae4282c7 Binary files /dev/null and b/desktop/src/main/resources/images/logo_landscape_light_mode.png differ diff --git a/desktop/src/main/resources/images/logo_splash.png b/desktop/src/main/resources/images/logo_splash.png deleted file mode 100644 index 110edbb85a..0000000000 Binary files a/desktop/src/main/resources/images/logo_splash.png and /dev/null differ diff --git a/desktop/src/main/resources/images/logo_splash@2x.png b/desktop/src/main/resources/images/logo_splash_dark_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash@2x.png rename to desktop/src/main/resources/images/logo_splash_dark_mode.png diff --git a/desktop/src/main/resources/images/logo_splash_light_mode.png b/desktop/src/main/resources/images/logo_splash_light_mode.png new file mode 100644 index 0000000000..45067473c9 Binary files /dev/null and b/desktop/src/main/resources/images/logo_splash_light_mode.png differ diff --git a/desktop/src/main/resources/images/logo_splash_testnet.png b/desktop/src/main/resources/images/logo_splash_testnet.png deleted file mode 100644 index 00e44cc5b1..0000000000 Binary files a/desktop/src/main/resources/images/logo_splash_testnet.png and /dev/null differ diff --git a/desktop/src/main/resources/images/logo_splash_testnet_dark_mode.png b/desktop/src/main/resources/images/logo_splash_testnet_dark_mode.png new file mode 100644 index 0000000000..c7f8fc10f1 Binary files /dev/null and b/desktop/src/main/resources/images/logo_splash_testnet_dark_mode.png differ diff --git a/desktop/src/main/resources/images/logo_splash_testnet@2x.png b/desktop/src/main/resources/images/logo_splash_testnet_light_mode.png similarity index 100% rename from desktop/src/main/resources/images/logo_splash_testnet@2x.png rename to desktop/src/main/resources/images/logo_splash_testnet_light_mode.png diff --git a/desktop/src/main/resources/images/ltc_logo.png b/desktop/src/main/resources/images/ltc_logo.png new file mode 100644 index 0000000000..0c7de04077 Binary files /dev/null and b/desktop/src/main/resources/images/ltc_logo.png differ diff --git a/desktop/src/main/resources/images/red_circle_solid.png b/desktop/src/main/resources/images/red_circle_solid.png new file mode 100644 index 0000000000..c5c182ed94 Binary files /dev/null and b/desktop/src/main/resources/images/red_circle_solid.png differ diff --git a/desktop/src/main/resources/images/alert_round@2x.png b/desktop/src/main/resources/images/red_circle_solid@2x.png similarity index 100% rename from desktop/src/main/resources/images/alert_round@2x.png rename to desktop/src/main/resources/images/red_circle_solid@2x.png diff --git a/desktop/src/main/resources/images/settings.png b/desktop/src/main/resources/images/settings.png new file mode 100644 index 0000000000..8a534387f0 Binary files /dev/null and b/desktop/src/main/resources/images/settings.png differ diff --git a/desktop/src/main/resources/images/sol_logo.png b/desktop/src/main/resources/images/sol_logo.png new file mode 100644 index 0000000000..26aec35be0 Binary files /dev/null and b/desktop/src/main/resources/images/sol_logo.png differ diff --git a/desktop/src/main/resources/images/support.png b/desktop/src/main/resources/images/support.png new file mode 100644 index 0000000000..b40bdd28e1 Binary files /dev/null and b/desktop/src/main/resources/images/support.png differ diff --git a/desktop/src/main/resources/images/trx_logo.png b/desktop/src/main/resources/images/trx_logo.png new file mode 100644 index 0000000000..bb2ab57421 Binary files /dev/null and b/desktop/src/main/resources/images/trx_logo.png differ diff --git a/desktop/src/main/resources/images/usdc-erc20_logo.png b/desktop/src/main/resources/images/usdc-erc20_logo.png new file mode 100644 index 0000000000..fb2bc4802d Binary files /dev/null and b/desktop/src/main/resources/images/usdc-erc20_logo.png differ diff --git a/desktop/src/main/resources/images/usdt-erc20_logo.png b/desktop/src/main/resources/images/usdt-erc20_logo.png new file mode 100644 index 0000000000..f1d7e2c9aa Binary files /dev/null and b/desktop/src/main/resources/images/usdt-erc20_logo.png differ diff --git a/desktop/src/main/resources/images/usdt-trc20_logo.png b/desktop/src/main/resources/images/usdt-trc20_logo.png new file mode 100644 index 0000000000..f1d7e2c9aa Binary files /dev/null and b/desktop/src/main/resources/images/usdt-trc20_logo.png differ diff --git a/desktop/src/main/resources/images/xmr_logo.png b/desktop/src/main/resources/images/xmr_logo.png new file mode 100644 index 0000000000..dc3d28ccce Binary files /dev/null and b/desktop/src/main/resources/images/xmr_logo.png differ diff --git a/desktop/src/main/resources/images/xrp_logo.png b/desktop/src/main/resources/images/xrp_logo.png new file mode 100644 index 0000000000..0c1034a6c6 Binary files /dev/null and b/desktop/src/main/resources/images/xrp_logo.png differ diff --git a/desktop/src/main/resources/images/yellow_circle.png b/desktop/src/main/resources/images/yellow_circle.png index 44e5a272fa..0fe6fc0b4e 100644 Binary files a/desktop/src/main/resources/images/yellow_circle.png and b/desktop/src/main/resources/images/yellow_circle.png differ diff --git a/desktop/src/main/resources/images/yellow_circle_solid.png b/desktop/src/main/resources/images/yellow_circle_solid.png new file mode 100644 index 0000000000..44e5a272fa Binary files /dev/null and b/desktop/src/main/resources/images/yellow_circle_solid.png differ diff --git a/desktop/src/test/java/haveno/desktop/ComponentsDemo.java b/desktop/src/test/java/haveno/desktop/ComponentsDemo.java index 740c2456b3..a73a0b5e43 100644 --- a/desktop/src/test/java/haveno/desktop/ComponentsDemo.java +++ b/desktop/src/test/java/haveno/desktop/ComponentsDemo.java @@ -91,6 +91,7 @@ public class ComponentsDemo extends Application { InputTextField inputTextField = FormBuilder.addInputTextField(gridPane, rowIndex++, "Enter something title"); inputTextField.setLabelFloat(true); + inputTextField.getStyleClass().add("label-float"); inputTextField.setText("Hello"); inputTextField.setPromptText("Enter something"); diff --git a/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java index 0aa4b545a7..09f9c466c4 100644 --- a/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModelTest.java @@ -88,7 +88,7 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(7, model.maxPlacesForBuyPrice.intValue()); + assertEquals(9, model.maxPlacesForBuyPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForBuyPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); @@ -117,11 +117,11 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(1, model.maxPlacesForBuyVolume.intValue()); //0 + assertEquals(3, model.maxPlacesForBuyVolume.intValue()); //0 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); - assertEquals(2, model.maxPlacesForBuyVolume.intValue()); //10 + assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //10 offerBookListItems.addAll(make(xmrBuyItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); - assertEquals(4, model.maxPlacesForBuyVolume.intValue()); //2213 + assertEquals(6, model.maxPlacesForBuyVolume.intValue()); //2213 } @Test @@ -166,7 +166,7 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(7, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price + assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 10.0000 default price offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 940164750000L)))); assertEquals(9, model.maxPlacesForSellPrice.intValue()); // 9401.6475 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.price, 1010164750000L)))); @@ -195,10 +195,10 @@ public class OfferBookChartViewModelTest { final OfferBookChartViewModel model = new OfferBookChartViewModel(offerBook, null, empty, service, null, null); model.activate(); - assertEquals(1, model.maxPlacesForSellVolume.intValue()); //0 + assertEquals(3, model.maxPlacesForSellVolume.intValue()); //0 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 1000000000000L)))); - assertEquals(2, model.maxPlacesForSellVolume.intValue()); //10 + assertEquals(4, model.maxPlacesForSellVolume.intValue()); //10 offerBookListItems.addAll(make(xmrSellItem.but(with(OfferBookListItemMaker.amount, 221286000000000L)))); - assertEquals(4, model.maxPlacesForSellVolume.intValue()); //2213 + assertEquals(6, model.maxPlacesForSellVolume.intValue()); //2213 } } diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 19756fc523..cf871529dc 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -217,7 +217,7 @@ public class CreateOfferViewModelTest { model.amount.set("0.01"); model.onFocusOutPriceAsPercentageTextField(true, false); //leave focus without changing assertEquals("0.00", model.marketPriceMargin.get()); - assertEquals("0.00000078", model.volume.get()); + assertEquals("126.84045000", model.volume.get()); assertEquals("12684.04500000", model.price.get()); } } diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java index eea8787a2f..cb98dcd763 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookListItemMaker.java @@ -42,9 +42,9 @@ public class OfferBookListItemMaker { public static final Instantiator OfferBookListItem = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( - with(OfferMaker.price, lookup.valueOf(price, 1000000000L)), - with(OfferMaker.amount, lookup.valueOf(amount, 1000000000L)), - with(OfferMaker.minAmount, lookup.valueOf(amount, 1000000000L)), + with(OfferMaker.price, lookup.valueOf(price, 100000000000L)), + with(OfferMaker.amount, lookup.valueOf(amount, 100000000000L)), + with(OfferMaker.minAmount, lookup.valueOf(amount, 100000000000L)), with(OfferMaker.direction, lookup.valueOf(direction, OfferDirection.BUY)), with(OfferMaker.useMarketBasedPrice, lookup.valueOf(useMarketBasedPrice, false)), with(OfferMaker.marketPriceMargin, lookup.valueOf(marketPriceMargin, 0.0)), @@ -56,8 +56,8 @@ public class OfferBookListItemMaker { public static final Instantiator OfferBookListItemWithRange = lookup -> new OfferBookListItem(make(xmrUsdOffer.but( MakeItEasy.with(OfferMaker.price, lookup.valueOf(price, 100000L)), - with(OfferMaker.minAmount, lookup.valueOf(minAmount, 1000000000L)), - with(OfferMaker.amount, lookup.valueOf(amount, 2000000000L))))); + with(OfferMaker.minAmount, lookup.valueOf(minAmount, 100000000000L)), + with(OfferMaker.amount, lookup.valueOf(amount, 200000000000L))))); public static final Maker xmrBuyItem = a(OfferBookListItem); public static final Maker xmrSellItem = a(OfferBookListItem, with(direction, OfferDirection.SELL)); diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java index b7cc852ee1..dfc68c4739 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -310,9 +310,9 @@ public class OfferBookViewModelTest { null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); - assertEquals(5, model.maxPlacesForVolume.intValue()); - offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); assertEquals(7, model.maxPlacesForVolume.intValue()); + offerBookListItems.addAll(make(xmrBuyItem.but(with(amount, 20000000000000L)))); + assertEquals(9, model.maxPlacesForVolume.intValue()); } @Test @@ -360,7 +360,7 @@ public class OfferBookViewModelTest { null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); - assertEquals(7, model.maxPlacesForPrice.intValue()); + assertEquals(9, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 1495582400000L)))); //149558240 assertEquals(10, model.maxPlacesForPrice.intValue()); offerBookListItems.addAll(make(xmrBuyItem.but(with(price, 149558240000L)))); //149558240 @@ -453,7 +453,7 @@ public class OfferBookViewModelTest { assertEquals("12557.2046", model.getPrice(lowItem)); assertEquals("(1.00%)", model.getPriceAsPercentage(lowItem)); - assertEquals("10.0000", model.getPrice(fixedItem)); + assertEquals("1000.0000", model.getPrice(fixedItem)); offerBookListItems.addAll(item); assertEquals("14206.1304", model.getPrice(item)); assertEquals("(-12.00%)", model.getPriceAsPercentage(item)); diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index 2496dbdbbd..28c2f52739 100644 --- a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java @@ -80,8 +80,8 @@ public class OfferMaker { lookup.valueOf(price, 100000L), lookup.valueOf(marketPriceMargin, 0.0), lookup.valueOf(useMarketBasedPrice, false), - lookup.valueOf(amount, 100000L), - lookup.valueOf(minAmount, 100000L), + lookup.valueOf(amount, 100000000000L), + lookup.valueOf(minAmount, 100000000000L), lookup.valueOf(makerFeePct, .0015), lookup.valueOf(takerFeePct, .0075), lookup.valueOf(penaltyFeePct, 0.03), @@ -97,7 +97,7 @@ public class OfferMaker { }}), null, null, - "2", + "3", lookup.valueOf(blockHeight, 700000L), lookup.valueOf(tradeLimit, 0L), lookup.valueOf(maxTradePeriod, 0L), diff --git a/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java b/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java index dc7d68c873..f5b00a7fb6 100644 --- a/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java +++ b/desktop/src/test/java/haveno/desktop/util/GUIUtilTest.java @@ -19,21 +19,15 @@ package haveno.desktop.util; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; -import haveno.core.locale.TradeCurrency; import haveno.core.trade.HavenoUtils; import haveno.core.user.DontShowAgainLookup; import haveno.core.user.Preferences; -import javafx.util.StringConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.math.BigInteger; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; -import static haveno.desktop.maker.TradeCurrencyMakers.euro; -import static haveno.desktop.maker.TradeCurrencyMakers.monero; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,22 +43,6 @@ public class GUIUtilTest { Res.setBaseCurrencyName("Bitcoin"); } - @Test - public void testTradeCurrencyConverter() { - Map offerCounts = new HashMap<>() {{ - put("XMR", 11); - put("EUR", 10); - }}; - StringConverter tradeCurrencyConverter = GUIUtil.getTradeCurrencyConverter( - Res.get("shared.oneOffer"), - Res.get("shared.multipleOffers"), - offerCounts - ); - - assertEquals("✦ Monero (XMR) - 11 offers", tradeCurrencyConverter.toString(monero)); - assertEquals("★ Euro (EUR) - 10 offers", tradeCurrencyConverter.toString(euro)); - } - @Test public void testOpenURLWithCampaignParameters() { Preferences preferences = mock(Preferences.class); diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index caa76ac1a9..76a09c62ff 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -20,4 +20,14 @@ When you have something new built for Haveno, submit a pull request for review t ## Developer guide -See the [developer guide](developer-guide.md) to get started developing Haveno. \ No newline at end of file +See the [developer guide](developer-guide.md) to get started developing Haveno. + +## Translation + +Existing translation files are in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/), feel free to update or improve them if needed. + +To add a new locale translations, follow these steps: + +- Add your [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) standard country language code to [core/src/main/java/haveno/core/locale/LanguageUtil.java](/core/src/main/java/haveno/core/locale/LanguageUtil.java) and remove it from "// not translated yet" if it is there. + +- Copy [displayStrings.properties](/core/src/main/resources/i18n/displayStrings.properties), create new file in [core/src/main/resources/i18n/](/core/src/main/resources/i18n/) in this format: `displayStrings_[insertLocaleName].properties` and then add translations. diff --git a/docs/create-mainnet.md b/docs/create-mainnet.md index 4b220e162e..4118a0f548 100644 --- a/docs/create-mainnet.md +++ b/docs/create-mainnet.md @@ -39,10 +39,10 @@ For demonstration, we can use the first generated public/private keypair for all Hardcode the public key(s) in these files: -- [AlertManager.java](https://github.com/haveno-dex/haveno/blob/1bf83ecb8baa06b6bfcc30720f165f20b8f77025/core/src/main/java/haveno/core/alert/AlertManager.java#L111) -- [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/1bf83ecb8baa06b6bfcc30720f165f20b8f77025/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L81) -- [FilterManager.java](https://github.com/haveno-dex/haveno/blob/1bf83ecb8baa06b6bfcc30720f165f20b8f77025/core/src/main/java/haveno/core/filter/FilterManager.java#L117) -- [PrivateNotificationManager.java](https://github.com/haveno-dex/haveno/blob/mainnet_placeholders/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java#L110) +- [AlertManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/AlertManager.java#L112) +- [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L81) +- [FilterManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/filter/FilterManager.java#L135) +- [PrivateNotificationManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java#L111) ## Change the default folder name for Haveno application data diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 67b5236d33..1001a725fd 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -5,10 +5,11 @@ This guide describes how to deploy a Haveno network: - Manage services on a VPS - Fork and build Haveno - Start a Monero node -- Build and start price nodes - Add seed nodes - Add arbitrators - Configure trade fees and other configuration +- Build and start price nodes +- Set a network filter - Build Haveno installers for distribution - Send alerts to update the application and other maintenance @@ -69,14 +70,6 @@ Optionally customize and deploy monero-stagenet.service and monero-stagenet.conf You can also start the Monero node in your current terminal session by running `make monerod` for mainnet or `make monerod-stagenet` for stagenet. -## Build and start price nodes - -The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. - -After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). - -Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service. - ## Add seed nodes ### Seed nodes without Proof of Work (PoW) @@ -139,7 +132,7 @@ Each seed node requires a locally running Monero node. You can use the default p Rebuild all seed nodes any time the list of registered seed nodes changes. -> **Notes** +> [!note] > * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, the network will be reset, including registered arbitrators, the network filter object, and trade history. In that case, arbitrators need to restart or re-register, and the network filter object needs to be re-applied. This should be done immediately or clients will cancel their offers due to the signing arbitrators being unregistered and no replacements being available to re-sign. > * At least 2 seed nodes should be run because the seed nodes restart once per day. @@ -149,21 +142,21 @@ Rebuild all seed nodes any time the list of registered seed nodes changes. 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for developers: `./gradlew generateKeypairs` -3. Add the developer public keys in the constructor of FilterManager.java. +3. Add the public key to `getPubKeyList()` in [FilterManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/filter/FilterManager.java#L135). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ### Register keypair(s) with alert privileges 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for alerts: `./gradlew generateKeypairs` -2. Add the public keys in the constructor of AlertManager.java. +3. Add the public key to `getPubKeyList()` in [AlertManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/AlertManager.java#L112). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ### Register keypair(s) with private notification privileges 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate public/private keypairs for private notifications: `./gradlew generateKeypairs` -2. Add the public keys in the constructor of PrivateNotificationManager.java. +3. Add the public key to `getPubKeyList()` in [PrivateNotificationManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/alert/PrivateNotificationManager.java#L111). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. ## Add arbitrators @@ -172,7 +165,7 @@ For each arbitrator: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. Generate a public/private keypair for the arbitrator: `./gradlew generateKeypairs` -3. Add the public key to `getPubKeyList()` in [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L62). +3. Add the public key to `getPubKeyList()` in [ArbitratorManager.java](https://github.com/haveno-dex/haveno/blob/2ff149b1ebcfd1a4c40d77d05d4ee9981353a8a6/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java#L81). 4. Update all seed nodes, arbitrators, and user applications for the change to take effect. 5. [Start a local Monero node](#start-a-local-monero-node). 6. Start the Haveno desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` @@ -180,32 +173,21 @@ For each arbitrator: The arbitrator is now registered and ready to accept requests for dispute resolution. -**Notes** -- Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. -- Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. -- Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. -- IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. +> [!note] +> * Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. +> * Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. +> * Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. +> * IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. ## Remove an arbitrator -> **Note** -> Ensure the arbitrator's trades are completed before retiring the instance. +> [!warning] +> * Ensure the arbitrator's trades are completed before retiring the instance. +> * To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking. 1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository. 2. Go to the `Account` tab and click the button to unregister the arbitrator. -## Set a network filter on mainnet - -On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. - -To set the network's filter object: - -1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. -2. Enter a developer private key from the previous steps and click "Add Filter" to register. - -> **Note** -> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered. - ## Change the default folder name for Haveno application data To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network: @@ -243,10 +225,30 @@ Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assig Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). +## Build and start price nodes + +The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode. + +After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50). + +Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service. + ## Update the download URL Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. +## Set a network filter on mainnet + +On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. + +To set the network's filter object: + +1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window. +2. Enter a developer private key from the previous steps and click "Add Filter" to register. + +> [!note] +> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered. + ## Start users for testing Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`. diff --git a/docs/installing.md b/docs/installing.md index eefd844ce6..6f341e7b9c 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -39,7 +39,7 @@ If it's the first time you are building Haveno, run the following commands to do ``` git clone https://github.com/haveno-dex/haveno.git cd haveno -git checkout master +git checkout v1.2.1 make ``` @@ -48,7 +48,7 @@ make If you are updating from a previous version, run from the root of the repository: ``` -git checkout master +git checkout v1.2.1 git pull make clean && make ``` diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 15194cfc52..d9fa2c2d56 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -820,9 +820,9 @@ - - - + + + @@ -2474,6 +2474,9 @@ + + + diff --git a/media/donate_monero.png b/media/donate_monero.png index 35b3e21d8f..83fe64fed6 100644 Binary files a/media/donate_monero.png and b/media/donate_monero.png differ diff --git a/p2p/src/main/java/haveno/network/p2p/AckMessage.java b/p2p/src/main/java/haveno/network/p2p/AckMessage.java index 7a7a0ff990..6d6470d568 100644 --- a/p2p/src/main/java/haveno/network/p2p/AckMessage.java +++ b/p2p/src/main/java/haveno/network/p2p/AckMessage.java @@ -53,6 +53,8 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, private final boolean success; @Nullable private final String errorMessage; + @Nullable + private final String updatedMultisigHex; /** * @@ -79,6 +81,27 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, sourceId, success, errorMessage, + null, + Version.getP2PMessageVersion()); + } + + public AckMessage(NodeAddress senderNodeAddress, + AckMessageSourceType sourceType, + String sourceMsgClassName, + String sourceUid, + String sourceId, + boolean success, + String errorMessage, + String updatedMultisigHex) { + this(UUID.randomUUID().toString(), + senderNodeAddress, + sourceType, + sourceMsgClassName, + sourceUid, + sourceId, + success, + errorMessage, + updatedMultisigHex, Version.getP2PMessageVersion()); } @@ -95,6 +118,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, String sourceId, boolean success, @Nullable String errorMessage, + String updatedMultisigInfo, String messageVersion) { super(messageVersion); this.uid = uid; @@ -105,6 +129,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, this.sourceId = sourceId; this.success = success; this.errorMessage = errorMessage; + this.updatedMultisigHex = updatedMultisigInfo; } public protobuf.AckMessage toProtoMessage() { @@ -126,6 +151,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, .setSuccess(success); Optional.ofNullable(sourceUid).ifPresent(builder::setSourceUid); Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); + Optional.ofNullable(updatedMultisigHex).ifPresent(builder::setUpdatedMultisigHex); return builder; } @@ -139,6 +165,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, proto.getSourceId(), proto.getSuccess(), proto.getErrorMessage().isEmpty() ? null : proto.getErrorMessage(), + proto.getUpdatedMultisigHex().isEmpty() ? null : proto.getUpdatedMultisigHex(), messageVersion); } @@ -163,6 +190,7 @@ public final class AckMessage extends NetworkEnvelope implements MailboxMessage, ",\n sourceId='" + sourceId + '\'' + ",\n success=" + success + ",\n errorMessage='" + errorMessage + '\'' + + ",\n updatedMultisigInfo='" + updatedMultisigHex + '\'' + "\n} " + super.toString(); } } diff --git a/p2p/src/main/java/haveno/network/p2p/P2PService.java b/p2p/src/main/java/haveno/network/p2p/P2PService.java index e7a810332e..ec296d641e 100644 --- a/p2p/src/main/java/haveno/network/p2p/P2PService.java +++ b/p2p/src/main/java/haveno/network/p2p/P2PService.java @@ -249,7 +249,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis requestDataManager.requestPreliminaryData(); keepAliveManager.start(); - p2pServiceListeners.forEach(SetupListener::onTorNodeReady); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(SetupListener::onTorNodeReady); + } } @Override @@ -258,17 +260,23 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis hiddenServicePublished.set(true); - p2pServiceListeners.forEach(SetupListener::onHiddenServicePublished); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(SetupListener::onHiddenServicePublished); + } } @Override public void onSetupFailed(Throwable throwable) { - p2pServiceListeners.forEach(e -> e.onSetupFailed(throwable)); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(e -> e.onSetupFailed(throwable)); + } } @Override public void onRequestCustomBridges() { - p2pServiceListeners.forEach(SetupListener::onRequestCustomBridges); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(SetupListener::onRequestCustomBridges); + } } // Called from networkReadyBinding @@ -304,7 +312,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onUpdatedDataReceived() { - p2pServiceListeners.forEach(P2PServiceListener::onUpdatedDataReceived); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(P2PServiceListener::onUpdatedDataReceived); + } } @Override @@ -314,7 +324,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onNoPeersAvailable() { - p2pServiceListeners.forEach(P2PServiceListener::onNoPeersAvailable); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(P2PServiceListener::onNoPeersAvailable); + } } @Override @@ -334,7 +346,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis mailboxMessageService.onBootstrapped(); // Once we have applied the state in the P2P domain we notify our listeners - p2pServiceListeners.forEach(listenerHandler); + synchronized (p2pServiceListeners) { + p2pServiceListeners.forEach(listenerHandler); + } mailboxMessageService.initAfterBootstrapped(); } @@ -369,12 +383,14 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis try { DecryptedMessageWithPubKey decryptedMsg = encryptionService.decryptAndVerify(sealedMsg.getSealedAndSigned()); connection.maybeHandleSupportedCapabilitiesMessage(decryptedMsg.getNetworkEnvelope()); - connection.getPeersNodeAddressOptional().ifPresentOrElse(nodeAddress -> - decryptedDirectMessageListeners.forEach(e -> e.onDirectMessage(decryptedMsg, nodeAddress)), - () -> { - log.error("peersNodeAddress is expected to be available at onMessage for " + - "processing PrefixedSealedAndSignedMessage."); - }); + connection.getPeersNodeAddressOptional().ifPresentOrElse(nodeAddress -> { + synchronized (decryptedDirectMessageListeners) { + decryptedDirectMessageListeners.forEach(e -> e.onDirectMessage(decryptedMsg, nodeAddress)); + } + }, () -> { + log.error("peersNodeAddress is expected to be available at onMessage for " + + "processing PrefixedSealedAndSignedMessage."); + }); } catch (CryptoException e) { log.warn("Decryption of a direct message failed. This is not expected as the " + "direct message was sent to our node."); @@ -503,19 +519,27 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis /////////////////////////////////////////////////////////////////////////////////////////// public void addDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { - decryptedDirectMessageListeners.add(listener); + synchronized (decryptedDirectMessageListeners) { + decryptedDirectMessageListeners.add(listener); + } } public void removeDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { - decryptedDirectMessageListeners.remove(listener); + synchronized (decryptedDirectMessageListeners) { + decryptedDirectMessageListeners.remove(listener); + } } public void addP2PServiceListener(P2PServiceListener listener) { - p2pServiceListeners.add(listener); + synchronized (p2pServiceListeners) { + p2pServiceListeners.add(listener); + } } public void removeP2PServiceListener(P2PServiceListener listener) { - p2pServiceListeners.remove(listener); + synchronized (p2pServiceListeners) { + p2pServiceListeners.remove(listener); + } } public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 79df171470..e01735f8dd 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -117,6 +117,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(240); private static final int SHUTDOWN_TIMEOUT = 100; private static final String THREAD_ID = Connection.class.getSimpleName(); + public static final int POSSIBLE_DOS_THRESHOLD = 5; public static int getPermittedMessageSize() { return PERMITTED_MESSAGE_SIZE; @@ -656,7 +657,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { if (logReport) { - if (numThrottledInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numThrottledInvalidRequestReports); + if (numThrottledInvalidRequestReports > 0) log.warn("We received {} throttled reports of invalid requests since the last log entry" + (numThrottledInvalidRequestReports >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledInvalidRequestReports); numThrottledInvalidRequestReports = 0; lastLoggedInvalidRequestReportTs = System.currentTimeMillis(); } @@ -872,7 +873,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { log.info("We got a {} from a peer with yet unknown address on connection with uid={}", networkEnvelope.getClass().getSimpleName(), uid); } - ThreadUtils.execute(() -> onMessage(networkEnvelope, this), THREAD_ID); + onMessage(networkEnvelope, this); ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID); } } catch (InvalidClassException e) { @@ -942,7 +943,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { @@ -954,7 +955,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean doLog = System.currentTimeMillis() - lastLoggedInfoTs > LOG_THROTTLE_INTERVAL_MS; if (doLog) { log.info(msg); - if (numThrottledInfos > 0) log.info("{} info logs were throttled since the last log entry", numThrottledInfos); + if (numThrottledInfos > 0) log.warn("We received {} throttled info logs since the last log entry" + (numThrottledInfos >= POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledInfos); numThrottledInfos = 0; lastLoggedInfoTs = System.currentTimeMillis(); } else { diff --git a/p2p/src/main/java/haveno/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/haveno/network/p2p/peers/Broadcaster.java index 647182feeb..d70d44311e 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/Broadcaster.java @@ -51,6 +51,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private boolean shutDownRequested; private Runnable shutDownResultHandler; private final ListeningExecutorService executor; + private final Object lock = new Object(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -76,13 +77,15 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { log.info("Broadcaster shutdown started"); shutDownRequested = true; shutDownResultHandler = resultHandler; - if (broadcastRequests.isEmpty()) { - doShutDown(); - } else { - // We set delay of broadcasts and timeout to very low values, - // so we can expect that we get onCompleted called very fast and trigger the - // doShutDown from there. - maybeBroadcastBundle(); + synchronized (lock) { + if (broadcastRequests.isEmpty()) { + doShutDown(); + } else { + // We set delay of broadcasts and timeout to very low values, + // so we can expect that we get onCompleted called very fast and trigger the + // doShutDown from there. + maybeBroadcastBundle(); + } } executor.shutdown(); } @@ -93,9 +96,11 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private void doShutDown() { log.info("Broadcaster doShutDown started"); - broadcastHandlers.forEach(BroadcastHandler::cancel); - if (timer != null) { - timer.stop(); + synchronized (lock) { + broadcastHandlers.forEach(BroadcastHandler::cancel); + if (timer != null) { + timer.stop(); + } } shutDownResultHandler.run(); } @@ -112,23 +117,27 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { - broadcastRequests.add(new BroadcastRequest(message, sender, listener)); - if (timer == null) { - timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); + synchronized (lock) { + broadcastRequests.add(new BroadcastRequest(message, sender, listener)); + if (timer == null) { + timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); + } } } private void maybeBroadcastBundle() { - if (!broadcastRequests.isEmpty()) { - BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); - broadcastHandlers.add(broadcastHandler); - broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested, executor); - broadcastRequests.clear(); + synchronized (lock) { + if (!broadcastRequests.isEmpty()) { + BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); + broadcastHandlers.add(broadcastHandler); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested, executor); + broadcastRequests.clear(); - if (timer != null) { - timer.stop(); + if (timer != null) { + timer.stop(); + } + timer = null; } - timer = null; } } @@ -138,9 +147,11 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { @Override public void onCompleted(BroadcastHandler broadcastHandler) { - broadcastHandlers.remove(broadcastHandler); - if (shutDownRequested) { - doShutDown(); + synchronized (lock) { + broadcastHandlers.remove(broadcastHandler); + if (shutDownRequested) { + doShutDown(); + } } } diff --git a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java index 07ea9397a5..4b81101bef 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/keepalive/KeepAliveHandler.java @@ -173,7 +173,7 @@ class KeepAliveHandler implements MessageListener { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= Connection.POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { diff --git a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java index 0b10307ccb..e28b92966f 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java @@ -222,7 +222,7 @@ class PeerExchangeHandler implements MessageListener { boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; if (logWarning) { log.warn(msg); - if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + if (numThrottledWarnings > 0) log.warn("We received {} throttled warnings since the last log entry" + (numThrottledWarnings >= Connection.POSSIBLE_DOS_THRESHOLD ? ". Possible DoS attack detected" : ""), numThrottledWarnings); numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { diff --git a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java index 40be21ef32..0517097190 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java @@ -187,7 +187,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + synchronized (persisted.getMap()) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + } completeHandler.run(); }, completeHandler); @@ -198,7 +200,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers public void readPersistedSync() { SequenceNumberMap persisted = persistenceManager.getPersisted(); if (persisted != null) { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + synchronized (persisted.getMap()) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(persisted.getMap())); + } } } @@ -641,9 +645,11 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers } removeFromMapAndDataStore(toRemoveList); - if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) { - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); - requestPersistence(); + synchronized (sequenceNumberMap.getMap()) { + if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) { + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); + requestPersistence(); + } } } } diff --git a/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java b/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java index f774c4c3c4..43cf2c1e1e 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/persistence/SequenceNumberMap.java @@ -19,8 +19,6 @@ package haveno.network.p2p.storage.persistence; import haveno.common.proto.persistable.PersistableEnvelope; import haveno.network.p2p.storage.P2PDataStorage; -import lombok.Getter; -import lombok.Setter; import java.util.HashMap; import java.util.Map; @@ -33,8 +31,6 @@ import java.util.stream.Collectors; * Hence this Persistable class. */ public class SequenceNumberMap implements PersistableEnvelope { - @Getter - @Setter private Map map = new ConcurrentHashMap<>(); public SequenceNumberMap() { @@ -46,20 +42,24 @@ public class SequenceNumberMap implements PersistableEnvelope { /////////////////////////////////////////////////////////////////////////////////////////// private SequenceNumberMap(Map map) { - this.map.putAll(map); + synchronized (this.map) { + this.map.putAll(map); + } } @Override public protobuf.PersistableEnvelope toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setSequenceNumberMap(protobuf.SequenceNumberMap.newBuilder() - .addAllSequenceNumberEntries(map.entrySet().stream() - .map(entry -> protobuf.SequenceNumberEntry.newBuilder() - .setBytes(entry.getKey().toProtoMessage()) - .setMapValue(entry.getValue().toProtoMessage()) - .build()) - .collect(Collectors.toList()))) - .build(); + synchronized (map) { + return protobuf.PersistableEnvelope.newBuilder() + .setSequenceNumberMap(protobuf.SequenceNumberMap.newBuilder() + .addAllSequenceNumberEntries(map.entrySet().stream() + .map(entry -> protobuf.SequenceNumberEntry.newBuilder() + .setBytes(entry.getKey().toProtoMessage()) + .setMapValue(entry.getValue().toProtoMessage()) + .build()) + .collect(Collectors.toList()))) + .build(); + } } public static SequenceNumberMap fromProto(protobuf.SequenceNumberMap proto) { @@ -74,20 +74,40 @@ public class SequenceNumberMap implements PersistableEnvelope { // API /////////////////////////////////////////////////////////////////////////////////////////// + public Map getMap() { + synchronized (map) { + return map; + } + } + + public void setMap(Map map) { + synchronized (this.map) { + this.map = map; + } + } + // Delegates public int size() { - return map.size(); + synchronized (map) { + return map.size(); + } } public boolean containsKey(P2PDataStorage.ByteArray key) { - return map.containsKey(key); + synchronized (map) { + return map.containsKey(key); + } } public P2PDataStorage.MapValue get(P2PDataStorage.ByteArray key) { - return map.get(key); + synchronized (map) { + return map.get(key); + } } public void put(P2PDataStorage.ByteArray key, P2PDataStorage.MapValue value) { - map.put(key, value); + synchronized (map) { + map.put(key, value); + } } } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b9615b5bcb..aa44587adf 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -896,11 +896,13 @@ message TradeInfo { bool is_deposits_published = 25; bool is_deposits_confirmed = 26; bool is_deposits_unlocked = 27; + bool is_deposits_finalized = 43; bool is_payment_sent = 28; bool is_payment_received = 29; bool is_payout_published = 30; bool is_payout_confirmed = 31; bool is_payout_unlocked = 32; + bool is_payout_finalized = 44; bool is_completed = 33; string contract_as_json = 34; ContractInfo contract = 35; @@ -908,6 +910,9 @@ message TradeInfo { string maker_deposit_tx_id = 37; string taker_deposit_tx_id = 38; string payout_tx_id = 39; + uint64 start_time = 40; + uint64 max_duration_ms = 41; + uint64 deadline_time = 42; } message ContractInfo { @@ -944,7 +949,9 @@ service Wallets { } rpc CreateXmrTx (CreateXmrTxRequest) returns (CreateXmrTxReply) { } - rpc relayXmrTx (RelayXmrTxRequest) returns (RelayXmrTxReply) { + rpc CreateXmrSweepTxs (CreateXmrSweepTxsRequest) returns (CreateXmrSweepTxsReply) { + } + rpc RelayXmrTxs (RelayXmrTxsRequest) returns (RelayXmrTxsReply) { } rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { } @@ -1036,12 +1043,20 @@ message CreateXmrTxReply { XmrTx tx = 1; } -message RelayXmrTxRequest { - string metadata = 1; +message CreateXmrSweepTxsRequest { + string address = 1; } -message RelayXmrTxReply { - string hash = 1; +message CreateXmrSweepTxsReply { + repeated XmrTx txs = 1; +} + +message RelayXmrTxsRequest { + repeated string metadatas = 1; +} + +message RelayXmrTxsReply { + repeated string hashes = 2; } message GetAddressBalanceRequest { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 5cdde1f0ce..dd232fa647 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -216,6 +216,7 @@ message AckMessage { string source_id = 6; // id of source (tradeId, disputeId) bool success = 7; // true if source message was processed successfully string error_message = 8; // optional error message if source message processing failed + string updated_multisig_hex = 9; // data to update the multisig state } message PrefixedSealedAndSignedMessage { @@ -336,6 +337,7 @@ message PaymentReceivedMessage { SignedWitness buyer_signed_witness = 9; PaymentSentMessage payment_sent_message = 10; bytes seller_signature = 11; + string payout_tx_id = 12; } message MediatedPayoutTxPublishedMessage { @@ -1046,7 +1048,7 @@ message FasterPaymentsAccountPayload { } message InteracETransferAccountPayload { - string email = 1; + string email_or_mobile_nr = 1; string holder_name = 2; string question = 3; string answer = 4; @@ -1162,7 +1164,7 @@ message TransferwiseAccountPayload { message TransferwiseUsdAccountPayload { string email = 1; string holder_name = 2; - string beneficiary_address = 3; + string holder_address = 3; } message PayseraAccountPayload { @@ -1455,6 +1457,7 @@ message Trade { DEPOSIT_TXS_SEEN_IN_NETWORK = 13; DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14; DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15; + DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN = 28; BUYER_CONFIRMED_PAYMENT_SENT = 16; BUYER_SENT_PAYMENT_SENT_MSG = 17; BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 18; @@ -1476,6 +1479,7 @@ message Trade { DEPOSITS_PUBLISHED = 3; DEPOSITS_CONFIRMED = 4; DEPOSITS_UNLOCKED = 5; + DEPOSITS_FINALIZED = 8; PAYMENT_SENT = 6; PAYMENT_RECEIVED = 7; } @@ -1485,6 +1489,7 @@ message Trade { PAYOUT_PUBLISHED = 1; PAYOUT_CONFIRMED = 2; PAYOUT_UNLOCKED = 3; + PAYOUT_FINALIZED = 4; } enum DisputeState { @@ -1584,6 +1589,8 @@ message ProcessModel { int64 trade_protocol_error_height = 18; string trade_fee_address = 19; bool import_multisig_hex_scheduled = 20; + bool payment_sent_payout_tx_stale = 21; + bool error_on_payment_received_msg = 22 [deprecated = true]; // used during debugging across clients (can be repurposed) } message TradePeer { @@ -1902,6 +1909,14 @@ message PaymentAccountForm { PAYPAL = 17; VENMO = 18; PAYSAFE = 19; + WECHAT_PAY = 20; + ALI_PAY = 21; + SWISH = 22; + TRANSFERWISE_USD = 23; + AMAZON_GIFT_CARD = 24; + ACH_TRANSFER = 25; + INTERAC_E_TRANSFER = 26; + US_POSTAL_MONEY_ORDER = 27; } FormId id = 1; repeated PaymentAccountFormField fields = 2; diff --git a/scripts/install_tails/README.md b/scripts/install_tails/README.md index 05b6470fd8..f4a700fab7 100644 --- a/scripts/install_tails/README.md +++ b/scripts/install_tails/README.md @@ -8,13 +8,13 @@ After you already have a [Tails USB](https://tails.net/install/linux/index.en.ht 4. Execute the following command in the terminal to download and execute the installation script. ``` - curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh + curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh ``` - Replace the binary zip URL and PGP fingerprint for the network you're using. For example: + Replace the installer URL and PGP fingerprint for the network you're using. For example: ``` - curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-linux-deb.zip FAA24D878B8D36C90120A897CA02DAC12DAE2D0F + curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-v1.2.1-linux-x86_64-installer.deb FAA24D878B8D36C90120A897CA02DAC12DAE2D0F ``` 5. Start Haveno by finding the icon in the launcher under **Applications > Other**. diff --git a/scripts/install_tails/assets/exec.sh b/scripts/install_tails/assets/exec.sh index ad0610a60b..55d821f386 100644 --- a/scripts/install_tails/assets/exec.sh +++ b/scripts/install_tails/assets/exec.sh @@ -59,4 +59,4 @@ fi echo_blue "Starting Haveno..." -/opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --userDataDir=${data_dir} --useTorForXmr=on --socks5ProxyXmrAddress=127.0.0.1:9050 +/opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --userDataDir=${data_dir} --useTorForXmr=on --socks5ProxyXmrAddress=127.0.0.1:9062 diff --git a/scripts/install_tails/haveno-install.sh b/scripts/install_tails/haveno-install.sh index e9a8c37bf4..16529f35e0 100755 --- a/scripts/install_tails/haveno-install.sh +++ b/scripts/install_tails/haveno-install.sh @@ -124,7 +124,7 @@ OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_fil if ! echo "$OUTPUT" | grep -q "Good signature from"; then echo_red "Verification failed: $OUTPUT" exit 1; - else 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}" + else mv -f "${binary_filename}" "${package_filename}" fi echo_blue "Haveno binaries have been successfully verified." @@ -136,7 +136,7 @@ mkdir -p "${install_dir}" # Delete old Haveno binaries #rm -f "${install_dir}/"*.deb* -mv "${binary_filename}" "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" +mv "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" echo_blue "Files moved to persistent directory ${install_dir}" diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md index c56b35cacd..924348b6eb 100644 --- a/scripts/install_whonix_qubes/INSTALL.md +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -147,13 +147,13 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ##### In `haveno-template` TemplateVM: ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

Example:

```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.2.1/haveno-v1.2.1-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` #### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* @@ -195,10 +195,9 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno ```shell # export https_proxy=http://127.0.0.1:8082 -# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt -# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig -# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip -# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

Note:

@@ -207,28 +206,22 @@ $ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno

For Whonix On Anything Other Than Qubes OS:

```shell -# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt -# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig -# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip -# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +# curl -sSLo /tmp/haveno.deb https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb +# curl -sSLo /tmp/haveno.deb.sig https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb.sig +# curl -sSLo /tmp/haveno-jar.SHA-256 https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-SNAPSHOT-all.jar.SHA-256 ```

Note:

Above are dummy URLS which MUST be replaced with actual working URLs

-###### Verify Release Files +###### Verify & Install Package File ```shell -# if gpg --digest-algo SHA256 --verify /tmp/hashes.txt.sig >/dev/null 2>&1; then printf $'SHASUM file has a VALID signature!\n'; else printf $'SHASUMS failed signature check\n' && sleep 5 && exit 1; fi -``` - -###### Verify Hash, Unpack & Install Package -```shell -# if [[ $(cat /tmp/hashes.txt) =~ $(sha512sum /tmp/haveno*.zip | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && mkdir -p /usr/share/desktop-directories && cd /tmp && unzip /tmp/haveno*.zip && apt install -y /tmp/haveno*.deb; else printf $'WARNING: Bad Hash!\n' && exit; fi +# if gpg --digest-algo SHA256 --verify /tmp/haveno.deb.sig >/dev/null 2>&1; then printf $'PACKAGE file has a VALID signature!\n' && mkdir -p /usr/share/desktop-directories && apt install -y /tmp/haveno*.deb; else printf $'PACKAGE failed signature check\n' && sleep 5 && exit 1; fi ``` ###### Verify Jar ```shell -# if [[ $(cat /tmp/desktop*.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi +# if [[ $(cat /tmp/haveno-jar.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi ``` #### *TemplateVM Building From Source via `git` Repository (Scripted)* diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md index 72670e41ca..35f5169f18 100644 --- a/scripts/install_whonix_qubes/README.md +++ b/scripts/install_whonix_qubes/README.md @@ -25,17 +25,17 @@ $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh ## **Build TemplateVM** -### *Via Binary Archive* +### *Via Package* #### **In `haveno-template` `TemplateVM`:** ```shell -% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" ```

Example:

```shell -% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/1.2.0/haveno-v1.2.0-linux-x86_64-installer.deb" "ABAF11C65A2970B130ABE3C479BE3E4300411886" ``` ### *Via Source* diff --git a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh index f1ab43ae1b..2dff819eaa 100644 --- a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh +++ b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh @@ -3,8 +3,8 @@ function remote { - if [[ -z $PRECOMPILED_URL || -z $FINGERPRINT ]]; then - printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nBinary URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" + if [[ -z $PACKAGE_URL || -z $FINGERPRINT ]]; then + printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nPackage URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" exit 1 fi ## Update & Upgrade @@ -32,12 +32,11 @@ function remote { ## Define URL & PGP Fingerprint etc. vars: - user_url=$PRECOMPILED_URL + user_url=$PACKAGE_URL base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') expected_fingerprint=$FINGERPRINT - binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") - package_filename="haveno.deb" - signature_filename="${binary_filename}.sig" + package_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") + signature_filename="${package_filename}.sig" key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" @@ -46,7 +45,6 @@ function remote { printf "\nUser URL=$user_url\n" printf "\nBase URL=$base_url\n" printf "\nFingerprint=$expected_fingerprint\n" - printf "\nBinary Name=$binary_filename\n" printf "\nPackage Name=$package_filename\n" printf "\nSig Filename=$signature_filename\n" printf "\nKey Filename=$key_filename\n" @@ -94,7 +92,7 @@ function remote { ## Verify the downloaded binary with the signature: echo_blue "Verifying the signature of the downloaded file ..." if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then - 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"; + mkdir -p /usr/share/desktop-directories; else echo_red "Verification failed!" && sleep 5 exit 1; fi @@ -172,7 +170,7 @@ if ! [[ $# -eq 2 || $# -eq 3 ]] ; then fi if [[ $# -eq 2 ]] ; then - PRECOMPILED_URL=$1 + PACKAGE_URL=$1 FINGERPRINT=$2 remote fi diff --git a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh index 11582a8314..52b4950ef6 100644 --- a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh +++ b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh @@ -1,5 +1,5 @@ #!/bin/zsh -## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh +## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh ## Function to print messages in blue: echo_blue() { @@ -42,7 +42,7 @@ whonix_firewall ### Create Desktop Launcher: echo_blue "Creating desktop launcher ..." mkdir -p /home/$(ls /home)/\.local/share/applications -sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --torControlUseSafeCookieAuth --torControlCookieFile=/var/run/tor/control.authcookie --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 35d4bbbe17..fa832ef833 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.19"; + private static final String VERSION = "1.2.0"; private SeedNode seedNode; private Timer checkConnectionLossTime;