diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0340da084..f1751c0d36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,6 @@ +# GitHub Releases requires a tag, e.g: +# git tag -s 1.0.19-1 -m "haveno-v1.0.19-1" +# git push origin 1.0.19-1 name: CI on: @@ -26,21 +29,23 @@ jobs: cache: gradle - name: Build with Gradle run: ./gradlew build --stacktrace --scan - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: error-reports-${{ matrix.os }} path: ${{ github.workspace }}/desktop/build/reports - name: cache nodes dependencies - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: + include-hidden-files: true name: cached-localnet path: .localnet + overwrite: true - name: Install dependencies if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - sudo apt update - sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream + sudo apt-get update + sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo - name: Install WiX Toolset if: ${{ matrix.os == 'windows-latest' }} @@ -67,10 +72,9 @@ jobs: "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append shell: powershell - - name: Move Release Files on Unix - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} + - name: Move Release Files for Linux + if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then mkdir ${{ github.workspace }}/release-linux-rpm mkdir ${{ github.workspace }}/release-linux-deb mkdir ${{ github.workspace }}/release-linux-flatpak @@ -83,58 +87,87 @@ jobs: cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-rpm cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-appimage cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-linux-flatpak - else + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + shell: bash + - name: Move Release Files for macOS + if: ${{ matrix.os == 'macos-13' }} + run: | mkdir ${{ github.workspace }}/release-macos mv desktop/build/temp-*/binaries/Haveno-*.dmg ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/release-macos - fi + cp desktop/build/temp-*/binaries/desktop-*.jar.SHA-256 ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 shell: bash - name: Move Release Files on Windows if: ${{ matrix.os == 'windows-latest' }} run: | mkdir ${{ github.workspace }}/release-windows Move-Item -Path desktop\build\temp-*/binaries\Haveno-*.exe -Destination ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe - Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows + Copy-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/release-windows + Move-Item -Path desktop\build\temp-*/binaries\desktop-*.jar.SHA-256 -Destination ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 shell: powershell # win - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Windows artifacts" - if: ${{ matrix.os == 'windows-latest'}} + if: ${{ matrix.os == 'windows-latest' }} with: name: haveno-windows path: ${{ github.workspace }}/release-windows # macos - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "macOS artifacts" - if: ${{ matrix.os == 'macos-13' }} + if: ${{ matrix.os == 'macos-13' }} with: name: haveno-macos path: ${{ github.workspace }}/release-macos # linux - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - deb artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-deb path: ${{ github.workspace }}/release-linux-deb - - uses: actions/upload-artifact@v3 + + - uses: actions/upload-artifact@v4 name: "Linux - rpm artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-rpm path: ${{ github.workspace }}/release-linux-rpm - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - AppImage artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-appimage path: ${{ github.workspace }}/release-linux-appimage - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 name: "Linux - flatpak artifact" if: ${{ matrix.os == 'ubuntu-22.04' }} with: name: haveno-linux-flatpak path: ${{ github.workspace }}/release-linux-flatpak + + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + ${{ github.workspace }}/release-linux-deb/haveno-v${{ env.VERSION }}-linux-x86_64-installer.deb + ${{ github.workspace }}/release-linux-rpm/haveno-v${{ env.VERSION }}-linux-x86_64-installer.rpm + ${{ github.workspace }}/release-linux-appimage/haveno-v${{ env.VERSION }}-linux-x86_64.AppImage + ${{ github.workspace }}/release-linux-flatpak/haveno-v${{ env.VERSION }}-linux-x86_64.flatpak + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-linux-x86_64-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/release-macos/haveno-v${{ env.VERSION }}-macos-installer.dmg + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-macos-SNAPSHOT-all.jar.SHA-256 + ${{ github.workspace }}/release-windows/haveno-v${{ env.VERSION }}-windows-installer.exe + ${{ github.workspace }}/haveno-v${{ env.VERSION }}-windows-SNAPSHOT-all.jar.SHA-256 + +# https://git-scm.com/docs/git-tag - git-tag Docu +# +# git tag - lists all local tags +# git tag -d 1.0.19-1 - delete local tag +# +# git ls-remote --tags - lists all remote tags +# git push origin --delete refs/tags/1.0.19-1 - delete remote tag diff --git a/.github/workflows/codacy-code-reporter.yml b/.github/workflows/codacy-code-reporter.yml index 1bf5b3cec5..be76ef35ef 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-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8b8699ad69..7e0fefe9e7 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-latest + runs-on: ubuntu-22.04 permissions: actions: read contents: read @@ -44,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,4 +68,4 @@ jobs: run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index d29b0e28eb..50ece9050c 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -7,7 +7,7 @@ on: jobs: issueLabeled: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Bounty explanation uses: peter-evans/create-or-update-comment@v3 diff --git a/LICENSE b/LICENSE index 93a5d000f0..90fb6f4689 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Copyright (C) 2020 Haveno Dex Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -659,4 +659,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. +. diff --git a/Makefile b/Makefile index 51d55af341..7bca61db60 100644 --- a/Makefile +++ b/Makefile @@ -70,9 +70,11 @@ monerod1-local: --log-level 0 \ --add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod2-local: ./.localnet/monerod \ @@ -88,9 +90,11 @@ monerod2-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod3-local: ./.localnet/monerod \ @@ -106,9 +110,11 @@ monerod3-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:48080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ #--proxy 127.0.0.1:49775 \ @@ -417,6 +423,17 @@ haveno-desktop-stagenet: --apiPort=3204 \ --useNativeXmrWallet=false \ +haveno-daemon-stagenet: + ./haveno-daemon$(APP_EXT) \ + --baseCurrencyNetwork=XMR_STAGENET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=Haveno \ + --apiPassword=apitest \ + --apiPort=3204 \ + --useNativeXmrWallet=false \ + # Mainnet network monerod: @@ -468,6 +485,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 \ diff --git a/README.md b/README.md index 7f4d1eb2eb..5b2e8015eb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@
Haveno logo - [![Codacy Badge](https://app.codacy.com/project/badge/Grade/505405b43cb74d5a996f106a3371588e)](https://app.codacy.com/gh/haveno-dex/haveno/dashboard) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master) - [![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=project%3Ahaveno-dex%2F2)](https://github.com/orgs/haveno-dex/projects/2) | + [![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)
@@ -14,7 +13,7 @@ Haveno (pronounced ha‧ve‧no) is an open source platform to exchange [Monero] Main features: -- All communications are routed through **Tor**, to preserve your privacy +- Communications are routed through **Tor**, to preserve your privacy. - Trades are **peer-to-peer**: trades on Haveno happen between people only, there is no central authority. @@ -24,31 +23,40 @@ Main features: See the [FAQ on our website](https://haveno.exchange/faq/) for more information. -## Status of the project +## Haveno Demo -Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time. +https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47 -A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network. +## Installing Haveno + +Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. + +> [!note] +> The official Haveno repository does not support making real trades directly. +> +> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. + +A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network. + +Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). Note that Haveno is being actively developed. If you find issues or bugs, please let us know. -Main repositories: +## Main repositories - **[haveno](https://github.com/haveno-dex/haveno)** - This repository. The core of Haveno. -- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - The user interface. - **[haveno-ts](https://github.com/haveno-dex/haveno-ts)** - TypeScript library for using Haveno. +- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - A new user interface (WIP). - **[haveno-meta](https://github.com/haveno-dex/haveno-meta)** - For project-wide discussions and proposals. -If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties) - -The PGP keys of the core team members are in `gpg_keys/`. +If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties). ## Keep in touch and help out! Haveno is a community-driven project. For it to be successful it's fundamental to have the support and help of the community. Join the community rooms on our Matrix server: - General discussions: **Haveno** ([#haveno:monero.social](https://matrix.to/#/#haveno:monero.social)) relayed on IRC/Libera (`#haveno`) -- Development discussions: **Haveno Development** ([#haveno-dev:monero.social](https://matrix.to/#/#haveno-dev:monero.social)) relayed on IRC/Libera (`#haveno-dev`) +- Development discussions: **Haveno Development** ([#haveno-development:monero.social](https://matrix.to/#/#haveno-development:monero.social)) relayed on IRC/Libera (`#haveno-development`) Email: contact@haveno.exchange Website: [haveno.exchange](https://haveno.exchange) @@ -59,28 +67,19 @@ 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) fund development bounties. +If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties. ## Bounties -To incentivize development and reward contributors we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the issues eligible for a bounty on the [dedicated Kanban board](https://github.com/orgs/haveno-dex/projects/2) or look for [issues labelled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue+is%3Aopen+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md). +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 To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project: -### Monero -

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. - -### Bitcoin - -

- Donate Bitcoin
- 1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ -

diff --git a/apitest/src/test/java/haveno/apitest/method/MethodTest.java b/apitest/src/test/java/haveno/apitest/method/MethodTest.java index 739c7c03c7..01c7a3bfd3 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.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; @@ -157,8 +157,8 @@ public class MethodTest extends ApiTestCase { return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER); } - public static final Supplier defaultBuyerSecurityDepositPct = () -> { - var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent()); + public static final Supplier defaultSecurityDepositPct = () -> { + var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent()); if (defaultPct.precision() != 2) throw new IllegalStateException(format( "Unexpected decimal precision, expected 2 but actual is %d%n." diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java index a7776dd683..1867605507 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java @@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest { 10000000L, 10000000L, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAccountId, NO_TRIGGER_PRICE); }; diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 38d83f696d..49c01d5ae4 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, "36000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId()); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, "30000.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId()); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, "29500.1234", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), eurAccount.getId()); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index cc6f53acdc..f4dff640c1 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer)); @@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 10_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), nzdAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer)); @@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), gbpAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer)); @@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), brlAccount.getId(), NO_TRIGGER_PRICE); log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer)); @@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { 10_000_000L, 5_000_000L, 0.0, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId(), triggerPrice); assertTrue(newOffer.getIsMyOffer()); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java index 4b7032c86f..8571c0c59d 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java @@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 75_000_000L, "0.005", // FIXED PRICE IN BTC FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 50_000_000L, "0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); assertTrue(newOffer.getIsMyOffer()); @@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 75_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), triggerPrice); log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer)); @@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest { 100_000_000L, 50_000_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer)); diff --git a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java index f299801c5a..e8745c1b2c 100644 --- a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java @@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 100000000000L, // exceeds amount limit 100000000000L, "10000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), usdAccount.getId())); assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage()); } @@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 10000000L, 10000000L, "40000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), chfAccount.getId())); String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId()); assertEquals(expectedError, exception.getMessage()); @@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest { 10000000L, 10000000L, "63000.0000", - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), audAccount.getId())); String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId()); assertEquals(expectedError, exception.getMessage()); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java index fee6e798ba..abbec38ff6 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java index 10be976c8b..3199a6b053 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest { 1_000_000L, 1_000_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesPaymentAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java index 40289e1d50..f61203400b 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java @@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest { 15_000_000L, 7_500_000L, "0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId()); log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); genBtcBlocksThenWait(1, 5000); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java index a425759717..4f43c21cd7 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java @@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { 12_500_000L, 12_500_000L, // min-amount = amount 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesUsdAccount.getId(), NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java index 68daa64050..9a769b5f04 100644 --- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java +++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java @@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest { 20_000_000L, 10_500_000L, priceMarginPctInput, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), alicesXmrAcct.getId(), NO_TRIGGER_PRICE); log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build()); diff --git a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java index 13b72ff79f..356b77ef8a 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("SELL offer {} created with margin based price {}.", @@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { 1_000_000, 1_000_000, 0.00, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), paymentAcct.getId(), triggerPrice); log.info("BUY offer {} created with margin based price {}.", diff --git a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java index eebaec8b04..4e8b86afac 100644 --- a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java +++ b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java @@ -28,7 +28,7 @@ import java.text.DecimalFormat; import java.util.Objects; import java.util.function.Supplier; -import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct; +import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct; import static haveno.cli.CurrencyFormat.formatInternalFiatPrice; import static haveno.cli.CurrencyFormat.formatSatoshis; import static haveno.common.util.MathUtils.scaleDownByPowerOf10; @@ -119,7 +119,7 @@ public class RandomOffer { amount, minAmount, priceMargin, - defaultBuyerSecurityDepositPct.get(), + defaultSecurityDepositPct.get(), "0" /*no trigger price*/); } else { this.offer = botClient.createOfferAtFixedPrice(paymentAccount, @@ -128,7 +128,7 @@ public class RandomOffer { amount, minAmount, fixedOfferPrice, - defaultBuyerSecurityDepositPct.get()); + defaultSecurityDepositPct.get()); } this.id = offer.getId(); return this; diff --git a/assets/src/main/java/haveno/asset/Trc20Token.java b/assets/src/main/java/haveno/asset/Trc20Token.java new file mode 100644 index 0000000000..3cffa34455 --- /dev/null +++ b/assets/src/main/java/haveno/asset/Trc20Token.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * Abstract base class for Tron-based {@link Token}s that implement the + * TRC-20 Token Standard. + */ +public abstract class Trc20Token extends Token { + + public Trc20Token(String name, String tickerSymbol) { + super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}")); + } +} diff --git a/assets/src/main/java/haveno/asset/package-info.java b/assets/src/main/java/haveno/asset/package-info.java index a50b986115..6eeaf4095f 100644 --- a/assets/src/main/java/haveno/asset/package-info.java +++ b/assets/src/main/java/haveno/asset/package-info.java @@ -21,7 +21,7 @@ * {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete * implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like * {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like - * {@link haveno.asset.tokens.DaiStablecoin}. + * {@link haveno.asset.tokens.DaiStablecoinERC20}. *

* The purpose of this package is to provide everything necessary for registering * ("listing") new assets and managing / accessing those assets within, e.g. the Haveno diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoin.java b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java similarity index 85% rename from assets/src/main/java/haveno/asset/tokens/USDCoin.java rename to assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java index b3e5c121e5..8c1e84e871 100644 --- a/assets/src/main/java/haveno/asset/tokens/USDCoin.java +++ b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java @@ -19,9 +19,9 @@ package haveno.asset.tokens; import haveno.asset.Erc20Token; -public class USDCoin extends Erc20Token { +public class DaiStablecoinERC20 extends Erc20Token { - public USDCoin() { - super("USD Coin", "USDC"); + public DaiStablecoinERC20() { + super("Dai Stablecoin", "DAI-ERC20"); } } diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java new file mode 100644 index 0000000000..1afb7ff1f2 --- /dev/null +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java @@ -0,0 +1,11 @@ +package haveno.asset.tokens; + +import haveno.asset.Erc20Token; + +public class TetherUSDERC20 extends Erc20Token { + public TetherUSDERC20() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (ERC20)", "USDT-ERC20"); + } +} diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java new file mode 100644 index 0000000000..c5669d126a --- /dev/null +++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java @@ -0,0 +1,11 @@ +package haveno.asset.tokens; + +import haveno.asset.Trc20Token; + +public class TetherUSDTRC20 extends Trc20Token { + public TetherUSDTRC20() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (TRC20)", "USDT-TRC20"); + } +} diff --git a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java similarity index 85% rename from assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java rename to assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java index e9cc01f74f..a65c021df9 100644 --- a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java +++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java @@ -19,9 +19,9 @@ package haveno.asset.tokens; import haveno.asset.Erc20Token; -public class DaiStablecoin extends Erc20Token { +public class USDCoinERC20 extends Erc20Token { - public DaiStablecoin() { - super("Dai Stablecoin", "DAI"); + public USDCoinERC20() { + super("USD Coin (ERC20)", "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 7108c28830..80b9cd036d 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -7,3 +7,7 @@ haveno.asset.coins.BitcoinCash haveno.asset.coins.Ether haveno.asset.coins.Litecoin haveno.asset.coins.Monero +haveno.asset.tokens.TetherUSDERC20 +haveno.asset.tokens.TetherUSDTRC20 +haveno.asset.tokens.USDCoinERC20 +haveno.asset.tokens.DaiStablecoinERC20 \ No newline at end of file diff --git a/assets/src/test/java/haveno/asset/coins/BitcoinTest.java b/assets/src/test/java/haveno/asset/coins/BitcoinTest.java index ad3bcee2c8..e90c25b6f7 100644 --- a/assets/src/test/java/haveno/asset/coins/BitcoinTest.java +++ b/assets/src/test/java/haveno/asset/coins/BitcoinTest.java @@ -32,6 +32,7 @@ public class BitcoinTest extends AbstractAssetTest { assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"); assertValidAddress("1111111111111111111114oLvT2"); assertValidAddress("1BitcoinEaterAddressDontSendf59kuE"); + assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw"); } @Test diff --git a/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java b/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java new file mode 100644 index 0000000000..fd7d97ce7d --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.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 haveno.asset.tokens.TetherUSDERC20; + +import org.junit.jupiter.api.Test; + + public class TetherUSDERC20Test extends AbstractAssetTest { + + public TetherUSDERC20Test() { + super(new TetherUSDERC20()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226"); + assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + } + } \ No newline at end of file diff --git a/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java b/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java new file mode 100644 index 0000000000..7fef554c75 --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.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 haveno.asset.tokens.TetherUSDTRC20; + +import org.junit.jupiter.api.Test; + + public class TetherUSDTRC20Test extends AbstractAssetTest { + + public TetherUSDTRC20Test() { + super(new TetherUSDTRC20()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266"); + assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g"); + } + } \ No newline at end of file diff --git a/build.gradle b/build.gradle index ce796bc2d9..0c8a4412f2 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.33' + moneroJavaVersion = '0.8.36' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' @@ -71,7 +71,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' - netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11 + netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' @@ -457,14 +457,14 @@ configure(project(':core')) { doLast { // get monero binaries download url Map moneroBinaries = [ - 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-x86_64.tar.gz', - 'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68', - 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz', - 'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de', - 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz', - 'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287', - 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip', - 'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888' + 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/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' ] String osKey @@ -506,16 +506,16 @@ configure(project(':core')) { } else { ext.extractArchiveTarGz(moneroArchiveFile, localnetDir) } + } - // add the current platform's monero dependencies into the resources folder for installation - copy { - from "${monerodFile}" - into "${project(':core').projectDir}/src/main/resources/bin" - } - copy { - from "${moneroRpcFile}" - into "${project(':core').projectDir}/src/main/resources/bin" - } + // add the current platform's monero dependencies into the resources folder for installation + copy { + from "${monerodFile}" + into "${project(':core').projectDir}/src/main/resources/bin" + } + copy { + from "${moneroRpcFile}" + into "${project(':core').projectDir}/src/main/resources/bin" } } @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.12-SNAPSHOT' + version = '1.1.1-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java index eaa0cac150..2fcb3426d1 100644 --- a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java @@ -81,7 +81,7 @@ public class OffersServiceRequest { .setUseMarketBasedPrice(useMarketBasedPrice) .setPrice(fixedPrice) .setMarketPriceMarginPct(marketPriceMarginPct) - .setBuyerSecurityDepositPct(securityDepositPct) + .setSecurityDepositPct(securityDepositPct) .setPaymentAccountId(paymentAcctId) .setTriggerPrice(triggerPrice) .build(); diff --git a/common/src/main/java/haveno/common/ClockWatcher.java b/common/src/main/java/haveno/common/ClockWatcher.java index 7bd8454c78..ba77ca38ab 100644 --- a/common/src/main/java/haveno/common/ClockWatcher.java +++ b/common/src/main/java/haveno/common/ClockWatcher.java @@ -69,7 +69,7 @@ public class ClockWatcher { listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { - log.info("We have been in standby mode for {} sec", missedMs / 1000); + log.warn("We have been in standby mode for {} sec", missedMs / 1000); listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs)); } } diff --git a/common/src/main/java/haveno/common/app/Capabilities.java b/common/src/main/java/haveno/common/app/Capabilities.java index 39266a25c6..5bb3bd0bc4 100644 --- a/common/src/main/java/haveno/common/app/Capabilities.java +++ b/common/src/main/java/haveno/common/app/Capabilities.java @@ -59,8 +59,10 @@ public class Capabilities { } public Capabilities(Collection capabilities) { - synchronized (this.capabilities) { - this.capabilities.addAll(capabilities); + synchronized (capabilities) { + synchronized (this.capabilities) { + this.capabilities.addAll(capabilities); + } } } @@ -73,9 +75,11 @@ public class Capabilities { } public void set(Collection capabilities) { - synchronized (this.capabilities) { - this.capabilities.clear(); - this.capabilities.addAll(capabilities); + synchronized (capabilities) { + synchronized (this.capabilities) { + this.capabilities.clear(); + this.capabilities.addAll(capabilities); + } } } @@ -87,15 +91,19 @@ public class Capabilities { public void addAll(Capabilities capabilities) { if (capabilities != null) { - synchronized (this.capabilities) { - this.capabilities.addAll(capabilities.capabilities); + synchronized (capabilities.capabilities) { + synchronized (this.capabilities) { + this.capabilities.addAll(capabilities.capabilities); + } } } } public boolean containsAll(final Set requiredItems) { - synchronized (this.capabilities) { - return capabilities.containsAll(requiredItems); + synchronized(requiredItems) { + synchronized (this.capabilities) { + return capabilities.containsAll(requiredItems); + } } } @@ -129,7 +137,9 @@ public class Capabilities { * @return int list of Capability ordinals */ public static List toIntList(Capabilities capabilities) { - return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); + synchronized (capabilities.capabilities) { + return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); + } } /** @@ -139,11 +149,13 @@ public class Capabilities { * @return a {@link Capabilities} object */ public static Capabilities fromIntList(List capabilities) { - return new Capabilities(capabilities.stream() - .filter(integer -> integer < Capability.values().length) - .filter(integer -> integer >= 0) - .map(integer -> Capability.values()[integer]) - .collect(Collectors.toSet())); + synchronized (capabilities) { + return new Capabilities(capabilities.stream() + .filter(integer -> integer < Capability.values().length) + .filter(integer -> integer >= 0) + .map(integer -> Capability.values()[integer]) + .collect(Collectors.toSet())); + } } /** @@ -181,7 +193,9 @@ public class Capabilities { } public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { - return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); + synchronized (capabilities.capabilities) { + return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); + } } @Override @@ -211,8 +225,10 @@ public class Capabilities { // Neither would support removal of past capabilities, a use case we never had so far and which might have // backward compatibility issues, so we should treat capabilities as an append-only data structure. public int findHighestCapability(Capabilities capabilities) { - return (int) capabilities.capabilities.stream() - .mapToLong(e -> (long) e.ordinal()) - .sum(); + synchronized (capabilities.capabilities) { + return (int) capabilities.capabilities.stream() + .mapToLong(e -> (long) e.ordinal()) + .sum(); + } } } diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 6708f17d79..d7be891aea 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.12"; + public static final String VERSION = "1.1.1"; /** * Holds a list of the tagged resource files for optimizing the getData requests. @@ -72,6 +72,25 @@ public class Version { return false; } + public static int compare(String version1, String version2) { + if (version1.equals(version2)) + return 0; + else if (getMajorVersion(version1) > getMajorVersion(version2)) + return 1; + else if (getMajorVersion(version1) < getMajorVersion(version2)) + return -1; + else if (getMinorVersion(version1) > getMinorVersion(version2)) + return 1; + else if (getMinorVersion(version1) < getMinorVersion(version2)) + return -1; + else if (getPatchVersion(version1) > getPatchVersion(version2)) + return 1; + else if (getPatchVersion(version1) < getPatchVersion(version2)) + return -1; + else + return 0; + } + private static int getSubVersion(String version, int index) { final String[] split = version.split("\\."); checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version); @@ -91,8 +110,9 @@ public class Version { // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening // the Haveno app. - // VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 - public static final int TRADE_PROTOCOL_VERSION = 1; + // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 + // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 + public static final int TRADE_PROTOCOL_VERSION = 2; 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 92359c76f9..b162e211b4 100644 --- a/common/src/main/java/haveno/common/config/Config.java +++ b/common/src/main/java/haveno/common/config/Config.java @@ -117,6 +117,8 @@ public class Config { public static final String BTC_FEE_INFO = "bitcoinFeeInfo"; public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation"; public static final String PASSWORD_REQUIRED = "passwordRequired"; + public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries"; + public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -204,6 +206,8 @@ public class Config { public final boolean republishMailboxEntries; public final boolean bypassMempoolValidation; public final boolean passwordRequired; + public final boolean updateXmrBinaries; + public final String xmrBlockchainPath; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -621,6 +625,20 @@ public class Config { .ofType(boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec updateXmrBinariesOpt = + parser.accepts(UPDATE_XMR_BINARIES, + "Update Monero binaries if applicable") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(true); + + ArgumentAcceptingOptionSpec xmrBlockchainPathOpt = + parser.accepts(XMR_BLOCKCHAIN_PATH, + "Path to Monero blockchain when using local Monero node") + .withRequiredArg() + .ofType(String.class) + .defaultsTo(""); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -733,6 +751,8 @@ public class Config { this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt); this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt); this.passwordRequired = options.valueOf(passwordRequiredOpt); + this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt); + this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), @@ -742,11 +762,11 @@ public class Config { } // Create all appDataDir subdirectories and assign to their respective properties - File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); - this.keyStorageDir = mkdir(btcNetworkDir, "keys"); - this.storageDir = mkdir(btcNetworkDir, "db"); - this.torDir = mkdir(btcNetworkDir, "tor"); - this.walletDir = mkdir(btcNetworkDir, "wallet"); + File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase()); + this.keyStorageDir = mkdir(xmrNetworkDir, "keys"); + this.storageDir = mkdir(xmrNetworkDir, "db"); + this.torDir = mkdir(xmrNetworkDir, "tor"); + this.walletDir = mkdir(xmrNetworkDir, "wallet"); // Assign values to special-case static fields APP_DATA_DIR_VALUE = appDataDir; diff --git a/common/src/main/java/haveno/common/crypto/KeyRing.java b/common/src/main/java/haveno/common/crypto/KeyRing.java index 89552c7c74..580b3b285a 100644 --- a/common/src/main/java/haveno/common/crypto/KeyRing.java +++ b/common/src/main/java/haveno/common/crypto/KeyRing.java @@ -110,7 +110,7 @@ public final class KeyRing { * @param password The password to unlock the keys or to generate new keys, nullable. */ public void generateKeys(String password) { - if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys"); + if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys"); symmetricKey = Encryption.generateSecretKey(256); signatureKeyPair = Sig.generateKeyPair(); encryptionKeyPair = Encryption.generateKeyPair(); diff --git a/common/src/main/java/haveno/common/crypto/KeyStorage.java b/common/src/main/java/haveno/common/crypto/KeyStorage.java index 05280b8d4a..5edfb89575 100644 --- a/common/src/main/java/haveno/common/crypto/KeyStorage.java +++ b/common/src/main/java/haveno/common/crypto/KeyStorage.java @@ -243,6 +243,11 @@ public class KeyStorage { //noinspection ResultOfMethodCallIgnored storageDir.mkdirs(); + // password must be ascii + if (password != null && !password.matches("\\p{ASCII}*")) { + throw new IllegalArgumentException("Password must be ASCII."); + } + var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray(); var passwordChars = password == null ? new char[0] : password.toCharArray(); try { diff --git a/common/src/main/java/haveno/common/file/FileUtil.java b/common/src/main/java/haveno/common/file/FileUtil.java index 27058f3025..ca533cc0d2 100644 --- a/common/src/main/java/haveno/common/file/FileUtil.java +++ b/common/src/main/java/haveno/common/file/FileUtil.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; @@ -74,17 +75,22 @@ public class FileUtil { } } - public static File getLatestBackupFile(File dir, String fileName) { + public static List getBackupFiles(File dir, String fileName) { File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString()); - if (!backupDir.exists()) return null; + if (!backupDir.exists()) return new ArrayList(); String dirName = "backups_" + fileName; if (dirName.contains(".")) dirName = dirName.replace(".", "_"); File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString()); - if (!backupFileDir.exists()) return null; + if (!backupFileDir.exists()) return new ArrayList(); File[] files = backupFileDir.listFiles(); - if (files == null || files.length == 0) return null; - Arrays.sort(files, Comparator.comparing(File::getName)); - return files[files.length - 1]; + return Arrays.asList(files); + } + + public static File getLatestBackupFile(File dir, String fileName) { + List files = getBackupFiles(dir, fileName); + if (files.isEmpty()) return null; + files.sort(Comparator.comparing(File::getName)); + return files.get(files.size() - 1); } public static void deleteRollingBackup(File dir, String fileName) { diff --git a/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java b/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java index 6e336c0036..d9af624c67 100644 --- a/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java +++ b/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java @@ -11,8 +11,8 @@ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . + * You should have received a copy of the GNU Affero General Public + * License along with Bisq. If not, see . */ package haveno.common.util; @@ -25,38 +25,67 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +/** + * Utility class for creating single-threaded executors. + */ public class SingleThreadExecutorUtils { + + private SingleThreadExecutorUtils() { + // Prevent instantiation + } + public static ExecutorService getSingleThreadExecutor(Class aClass) { - String name = aClass.getSimpleName(); - return getSingleThreadExecutor(name); + validateClass(aClass); + return getSingleThreadExecutor(aClass.getSimpleName()); } public static ExecutorService getNonDaemonSingleThreadExecutor(Class aClass) { - String name = aClass.getSimpleName(); - return getSingleThreadExecutor(name, false); + validateClass(aClass); + return getSingleThreadExecutor(aClass.getSimpleName(), false); } public static ExecutorService getSingleThreadExecutor(String name) { + validateName(name); return getSingleThreadExecutor(name, true); } public static ListeningExecutorService getSingleThreadListeningExecutor(String name) { + validateName(name); return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name)); } public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) { + validateThreadFactory(threadFactory); return Executors.newSingleThreadExecutor(threadFactory); } private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) { - final ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread); + ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread); return Executors.newSingleThreadExecutor(threadFactory); } private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) { return new ThreadFactoryBuilder() - .setNameFormat(name) + .setNameFormat(name + "-%d") .setDaemon(isDaemonThread) .build(); } + + private static void validateClass(Class aClass) { + if (aClass == null) { + throw new IllegalArgumentException("Class must not be null."); + } + } + + private static void validateName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name must not be null or empty."); + } + } + + private static void validateThreadFactory(ThreadFactory threadFactory) { + if (threadFactory == null) { + throw new IllegalArgumentException("ThreadFactory must not be null."); + } + } } 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 deab92eb50..978b6dd715 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java @@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection; import haveno.core.offer.OfferRestrictions; import haveno.core.payment.ChargeBackRisk; import haveno.core.payment.PaymentAccount; +import haveno.core.payment.TradeLimits; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.payload.PaymentMethod; import haveno.core.support.dispute.Dispute; @@ -498,10 +499,15 @@ public class AccountAgeWitnessService { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } - public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) { + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) { if (paymentAccount == null) return 0; + if (buyerAsTakerWithoutDeposit) { + TradeLimits tradeLimits = new TradeLimits(); + return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact(); + } + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode); if (hasTradeLimitException(accountAgeWitness)) { @@ -737,14 +743,13 @@ public class AccountAgeWitnessService { } public Optional traderSignAndPublishPeersAccountAgeWitness(Trade trade) { - AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); - BigInteger tradeAmount = trade.getAmount(); checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring"); PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey(); - checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}", - trade.toString()); - checkNotNull(tradeAmount, "Trade amount must not be null"); checkNotNull(peersPubKey, "Peers pub key must not be null"); + AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); + checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString()); + BigInteger tradeAmount = trade.getAmount(); + checkNotNull(tradeAmount, "Trade amount must not be null"); try { return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 14ec0f6d08..e8e83978eb 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -239,8 +239,8 @@ public class CoreApi { xmrConnectionService.stopCheckingConnection(); } - public MoneroRpcConnection getBestAvailableXmrConnection() { - return xmrConnectionService.getBestAvailableConnection(); + public MoneroRpcConnection getBestXmrConnection() { + return xmrConnectionService.getBestConnection(); } public void setXmrConnectionAutoSwitch(boolean autoSwitch) { @@ -413,18 +413,22 @@ public class CoreApi { } public void postOffer(String currencyCode, - String directionAsString, - String priceAsString, - boolean useMarketBasedPrice, - double marketPriceMargin, - long amountAsLong, - long minAmountAsLong, - double buyerSecurityDeposit, - String triggerPriceAsString, - boolean reserveExactAmount, - String paymentAccountId, - Consumer resultHandler, - ErrorMessageHandler errorMessageHandler) { + String directionAsString, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long amountAsLong, + long minAmountAsLong, + double securityDepositPct, + String triggerPriceAsString, + boolean reserveExactAmount, + String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, + String extraInfo, + String sourceOfferId, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { coreOffersService.postOffer(currencyCode, directionAsString, priceAsString, @@ -432,10 +436,14 @@ public class CoreApi { marketPriceMargin, amountAsLong, minAmountAsLong, - buyerSecurityDeposit, + securityDepositPct, triggerPriceAsString, reserveExactAmount, paymentAccountId, + isPrivateOffer, + buyerAsTakerWithoutDeposit, + extraInfo, + sourceOfferId, resultHandler, errorMessageHandler); } @@ -448,8 +456,11 @@ public class CoreApi { double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, + String extraInfo) { return coreOffersService.editOffer(offerId, currencyCode, direction, @@ -458,8 +469,11 @@ public class CoreApi { marketPriceMargin, amount, minAmount, - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit, + extraInfo); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -535,9 +549,11 @@ public class CoreApi { public void takeOffer(String offerId, String paymentAccountId, long amountAsLong, + String challenge, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = coreOffersService.getOffer(offerId); + offer.setChallenge(challenge); coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index f193287edc..f4bb4c803d 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreDisputesService { - public enum DisputePayout { + // TODO: persist in DisputeResult? + public enum PayoutSuggestion { BUYER_GETS_TRADE_AMOUNT, - BUYER_GETS_ALL, // used in desktop + BUYER_GETS_ALL, SELLER_GETS_TRADE_AMOUNT, - SELLER_GETS_ALL, // used in desktop + SELLER_GETS_ALL, CUSTOM } @@ -172,17 +173,17 @@ public class CoreDisputesService { // create dispute result var closeDate = new Date(); var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); - DisputePayout payout; + PayoutSuggestion payoutSuggestion; if (customWinnerAmount > 0) { - payout = DisputePayout.CUSTOM; + payoutSuggestion = PayoutSuggestion.CUSTOM; } else if (winner == DisputeResult.Winner.BUYER) { - payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; + payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; } else if (winner == DisputeResult.Winner.SELLER) { - payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; + payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; } else { throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); } - applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount); + applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount); // close winning dispute ticket closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> { @@ -227,26 +228,26 @@ public class CoreDisputesService { * Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer * receives the remaining amount minus the mining fee. */ - public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { + public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { Contract contract = dispute.getContract(); Trade trade = tradeManager.getTrade(dispute.getTradeId()); BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); BigInteger tradeAmount = contract.getTradeAmount(); disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER); - if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) { + if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit)); disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit); - } else if (payout == DisputePayout.BUYER_GETS_ALL) { + } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7) disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO); - } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { + } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit)); - } else if (payout == DisputePayout.SELLER_GETS_ALL) { + } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) { disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO); disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit)); - } else if (payout == DisputePayout.CUSTOM) { + } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) { if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance"); long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact(); if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative"); diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 5bdd2f1605..3ee7e047f1 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply; import static haveno.common.util.MathUtils.roundDoubleToLong; import static haveno.common.util.MathUtils.scaleUpByPowerOf10; import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; @@ -66,9 +67,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Comparator; import static java.util.Comparator.comparing; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -124,7 +123,6 @@ public class CoreOffersService { return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; }) .collect(Collectors.toList()); - offers.removeAll(getOffersWithDuplicateKeyImages(offers)); return offers; } @@ -143,12 +141,9 @@ public class CoreOffersService { } List getMyOffers() { - List offers = openOfferManager.getOpenOffers().stream() + return openOfferManager.getOpenOffers().stream() .filter(o -> o.getOffer().isMyOffer(keyRing)) .collect(Collectors.toList()); - Set offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images - Set offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet()); - return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList()); }; List getMyOffers(String direction, String currencyCode) { @@ -159,7 +154,7 @@ public class CoreOffersService { } OpenOffer getMyOffer(String id) { - return openOfferManager.getOpenOfferById(id) + return openOfferManager.getOpenOffer(id) .filter(open -> open.getOffer().isMyOffer(keyRing)) .orElseThrow(() -> new IllegalStateException(format("openoffer with id '%s' not found", id))); @@ -172,19 +167,38 @@ public class CoreOffersService { double marketPriceMargin, long amountAsLong, long minAmountAsLong, - double securityDeposit, + double securityDepositPct, String triggerPriceAsString, boolean reserveExactAmount, String paymentAccountId, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, + String extraInfo, + String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); - if (paymentAccount == null) - throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); + if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId)); + // clone offer if sourceOfferId given + if (!sourceOfferId.isEmpty()) { + cloneOffer(sourceOfferId, + currencyCode, + priceAsString, + useMarketBasedPrice, + marketPriceMargin, + triggerPriceAsString, + paymentAccountId, + extraInfo, + resultHandler, + errorMessageHandler); + return; + } + + // create new offer String upperCaseCurrencyCode = currencyCode.toUpperCase(); String offerId = createOfferService.getRandomOfferId(); OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase()); @@ -199,22 +213,78 @@ public class CoreOffersService { price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - securityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit, + extraInfo); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); - // We don't support atm funding from external wallet to keep it simple. - boolean useSavingsWallet = true; - //noinspection ConstantConditions placeOffer(offer, triggerPriceAsString, - useSavingsWallet, + true, reserveExactAmount, + null, transaction -> resultHandler.accept(offer), errorMessageHandler); } + private void cloneOffer(String sourceOfferId, + String currencyCode, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + String triggerPriceAsString, + String paymentAccountId, + String extraInfo, + Consumer resultHandler, + ErrorMessageHandler errorMessageHandler) { + + // get source offer + OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId); + Offer sourceOffer = sourceOpenOffer.getOffer(); + + // get trade currency (default source currency) + if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode(); + if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode(); + String upperCaseCurrencyCode = currencyCode.toUpperCase(); + + // get price (default source price) + Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString); + if (price == null) useMarketBasedPrice = true; + + // get payment account + if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId(); + PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId); + if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId)); + + // get extra info + if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo(); + + // create cloned offer + Offer offer = createOfferService.createClonedOffer(sourceOffer, + upperCaseCurrencyCode, + price, + useMarketBasedPrice, + exactMultiply(marketPriceMargin, 0.01), + paymentAccount, + extraInfo); + + // verify cloned offer + verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); + + // place offer + placeOffer(offer, + triggerPriceAsString, + true, + false, // ignored when cloning + sourceOfferId, + transaction -> resultHandler.accept(offer), + errorMessageHandler); + } + + // TODO: this implementation is missing; implement. Offer editOffer(String offerId, String currencyCode, OfferDirection direction, @@ -223,8 +293,11 @@ public class CoreOffersService { double marketPriceMargin, BigInteger amount, BigInteger minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + double securityDepositPct, + PaymentAccount paymentAccount, + boolean isPrivateOffer, + boolean buyerAsTakerWithoutDeposit, + String extraInfo) { return createOfferService.createAndGetOffer(offerId, direction, currencyCode.toUpperCase(), @@ -233,8 +306,11 @@ public class CoreOffersService { price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), - buyerSecurityDeposit, - paymentAccount); + securityDepositPct, + paymentAccount, + isPrivateOffer, + buyerAsTakerWithoutDeposit, + extraInfo); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -244,26 +320,6 @@ public class CoreOffersService { // -------------------------- PRIVATE HELPERS ----------------------------- - private Set getOffersWithDuplicateKeyImages(List offers) { - Set duplicateFundedOffers = new HashSet(); - Set seenKeyImages = new HashSet(); - for (Offer offer : offers) { - if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue; - for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (!seenKeyImages.add(keyImage)) { - for (Offer offer2 : offers) { - if (offer == offer2) continue; - if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { - log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId()); - duplicateFundedOffers.add(offer2); - } - } - } - } - } - return duplicateFundedOffers; - } - private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { String error = format("cannot create %s offer with payment account %s", @@ -277,6 +333,7 @@ public class CoreOffersService { String triggerPriceAsString, boolean useSavingsWallet, boolean reserveExactAmount, + String sourceOfferId, Consumer resultHandler, ErrorMessageHandler errorMessageHandler) { long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode()); @@ -285,6 +342,7 @@ public class CoreOffersService { triggerPriceAsLong, reserveExactAmount, true, + sourceOfferId, resultHandler::accept, errorMessageHandler); } diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java index dcf1ef0e74..b506a00ac1 100644 --- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java @@ -64,9 +64,14 @@ class CorePaymentAccountsService { } PaymentAccount createPaymentAccount(PaymentAccountForm form) { + validateFormFields(form); PaymentAccount paymentAccount = form.toPaymentAccount(); setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload verifyPaymentAccountHasRequiredFields(paymentAccount); + if (paymentAccount instanceof CryptoCurrencyAccount) { + CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount; + verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress()); + } user.addPaymentAccountIfNotExists(paymentAccount); accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload()); log.info("Saved payment account with id {} and payment method {}.", @@ -166,6 +171,12 @@ class CorePaymentAccountsService { .collect(Collectors.toList()); } + private void validateFormFields(PaymentAccountForm form) { + for (PaymentAccountFormField field : form.getFields()) { + validateFormField(form, field.getId(), field.getValue()); + } + } + void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) { // get payment method id diff --git a/core/src/main/java/haveno/core/api/CorePriceService.java b/core/src/main/java/haveno/core/api/CorePriceService.java index 453236c5bc..ddd194ebab 100644 --- a/core/src/main/java/haveno/core/api/CorePriceService.java +++ b/core/src/main/java/haveno/core/api/CorePriceService.java @@ -72,7 +72,7 @@ class CorePriceService { * @return Price per 1 XMR in the given currency (traditional or crypto) */ public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException { - var marketPrice = priceFeedService.requestAllPrices().get(currencyCode); + var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode)); if (marketPrice == null) { throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client } diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 431ab9a652..5e53ff64e4 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage; import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.ClosedTradableManager; -import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeUtil; @@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol; import haveno.core.trade.protocol.SellerProtocol; import haveno.core.user.User; import haveno.core.util.coin.CoinUtil; -import haveno.core.util.validation.BtcAddressValidator; -import haveno.core.xmr.model.AddressEntry; -import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT; import haveno.core.xmr.wallet.BtcWalletService; import static java.lang.String.format; import java.math.BigInteger; @@ -68,7 +64,6 @@ import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.bitcoinj.core.Coin; @Singleton @Slf4j @@ -84,7 +79,6 @@ class CoreTradesService { private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; private final TraderChatManager traderChatManager; - private final TradeUtil tradeUtil; private final OfferUtil offerUtil; private final User user; @@ -106,7 +100,6 @@ class CoreTradesService { this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; this.traderChatManager = traderChatManager; - this.tradeUtil = tradeUtil; this.offerUtil = offerUtil; this.user = user; } @@ -132,7 +125,7 @@ class CoreTradesService { // adjust amount for fixed-price offer (based on TakeOfferViewModel) String currencyCode = offer.getCurrencyCode(); OfferDirection direction = offer.getOfferPayload().getDirection(); - long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit()); if (offer.getPrice() != null) { if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) { amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit); @@ -206,7 +199,7 @@ class CoreTradesService { String getTradeRole(String tradeId) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); - return tradeUtil.getRole(getTrade(tradeId)); + return TradeUtil.getRole(getTrade(tradeId)); } Trade getTrade(String tradeId) { @@ -223,8 +216,7 @@ class CoreTradesService { } private Optional getClosedTrade(String tradeId) { - Optional tradable = closedTradableManager.getTradeById(tradeId); - return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value); + return closedTradableManager.getTradeById(tradeId); } List getTrades() { @@ -267,40 +259,9 @@ class CoreTradesService { return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; } - private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) { - // TODO This and identical logic should be refactored into TradeUtil. - try { - return btcWalletService.getFeeEstimationTransaction(fromAddress, - toAddress, - amount, - TRADE_PAYOUT).getFee(); - } catch (Exception ex) { - log.error("", ex); - throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage())); - } - } - // Throws a RuntimeException trade is already closed. private void verifyTradeIsNotClosed(String tradeId) { if (getClosedTrade(tradeId).isPresent()) throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId)); } - - // Throws a RuntimeException if address is not valid. - private void verifyIsValidBTCAddress(String address) { - try { - new BtcAddressValidator().validate(address); - } catch (Throwable t) { - log.error("", t); - throw new IllegalArgumentException(format("'%s' is not a valid btc address", address)); - } - } - - // Throws a RuntimeException if address has a zero balance. - private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) { - Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress()); - if (fromAddressBalance.isZero()) - throw new IllegalStateException(format("funds already withdrawn from address '%s'", - fromAddressEntry.getAddressString())); - } } diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 75be338503..53eba276a0 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -32,6 +32,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode; import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.setup.DownloadListener; import haveno.core.xmr.setup.WalletsSetup; +import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; @@ -40,7 +41,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -64,7 +64,6 @@ import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; import monero.daemon.model.MoneroDaemonInfo; -import monero.daemon.model.MoneroPeer; @Slf4j @Singleton @@ -73,6 +72,14 @@ public final class XmrConnectionService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds + private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes + + public enum XmrConnectionFallbackType { + LOCAL, + CUSTOM, + PROVIDED + } private final Object lock = new Object(); private final Object pollLock = new Object(); @@ -85,12 +92,14 @@ public final class XmrConnectionService { private final XmrLocalNode xmrLocalNode; private final MoneroConnectionManager connectionManager; private final EncryptedConnectionList connectionList; - private final ObjectProperty> peers = new SimpleObjectProperty<>(); + private final ObjectProperty> connections = new SimpleObjectProperty<>(); + private final IntegerProperty numConnections = new SimpleIntegerProperty(0); private final ObjectProperty connectionProperty = new SimpleObjectProperty<>(); - private final IntegerProperty numPeers = new SimpleIntegerProperty(0); private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter + private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>(); + @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); private Socks5ProxyProvider socks5ProxyProvider; @@ -101,6 +110,7 @@ public final class XmrConnectionService { private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; + private Long lastFallbackInvocation; private Long lastLogPollErrorTimestamp; private long lastLogDaemonNotSyncedTimestamp; private Long syncStartHeight; @@ -109,6 +119,7 @@ public final class XmrConnectionService { @Getter private boolean isShutDownStarted; private List listeners = new ArrayList<>(); + private XmrKeyImagePoller keyImagePoller; // connection switching private static final int EXCLUDE_CONNECTION_SECONDS = 180; @@ -117,6 +128,9 @@ public final class XmrConnectionService { private int numRequestsLastMinute; private long lastSwitchTimestamp; 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, @@ -144,7 +158,13 @@ public final class XmrConnectionService { p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { - initialize(); + ThreadUtils.submitToPool(() -> { + try { + initialize(); + } catch (Exception e) { + log.warn("Error initializing connection service, error={}\n", e.getMessage(), e); + } + }); } @Override public void onHiddenServicePublished() {} @@ -250,18 +270,29 @@ public final class XmrConnectionService { updatePolling(); } - public MoneroRpcConnection getBestAvailableConnection() { - accountService.checkAccountOpen(); - List ignoredConnections = new ArrayList(); - addLocalNodeIfIgnored(ignoredConnections); - return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0])); + public MoneroRpcConnection getBestConnection() { + return getBestConnection(new ArrayList()); } - private MoneroRpcConnection getBestAvailableConnection(Collection ignoredConnections) { + private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); + + // user needs to authorize fallback on startup after using locally synced node + if (fallbackRequiredBeforeConnectionSwitch()) { + log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); + return null; + } + + // get best connection Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); - return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); + MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections + if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0); + return bestConnection; + } + + private boolean fallbackRequiredBeforeConnectionSwitch() { + return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored()); } private void addLocalNodeIfIgnored(Collection ignoredConnections) { @@ -273,7 +304,7 @@ public final class XmrConnectionService { log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled"); return; } - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); } @@ -324,7 +355,7 @@ public final class XmrConnectionService { if (currentConnection != null) excludedConnections.add(currentConnection); // get connection to switch to - MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); + MoneroRpcConnection bestConnection = getBestConnection(excludedConnections); // remove from excluded connections after period UserThread.runAfter(() -> { @@ -332,7 +363,7 @@ public final class XmrConnectionService { }, EXCLUDE_CONNECTION_SECONDS); // return if no connection to switch to - if (bestConnection == null) { + if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) { log.warn("No connection to switch to"); return false; } @@ -388,14 +419,25 @@ public final class XmrConnectionService { return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced } - // ----------------------------- APP METHODS ------------------------------ - - public ReadOnlyIntegerProperty numPeersProperty() { - return numPeers; + public XmrKeyImagePoller getKeyImagePoller() { + synchronized (lock) { + if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller(); + return keyImagePoller; + } } - public ReadOnlyObjectProperty> peerConnectionsProperty() { - return peers; + private long getKeyImageRefreshPeriodMs() { + return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; + } + + // ----------------------------- APP METHODS ------------------------------ + + public ReadOnlyIntegerProperty numConnectionsProperty() { + return numConnections; + } + + public ReadOnlyObjectProperty> connectionsProperty() { + return connections; } public ReadOnlyObjectProperty connectionProperty() { @@ -403,7 +445,7 @@ public final class XmrConnectionService { } public boolean hasSufficientPeersForBroadcast() { - return numPeers.get() >= getMinBroadcastConnections(); + return numConnections.get() >= getMinBroadcastConnections(); } public LongProperty chainHeightProperty() { @@ -426,6 +468,24 @@ public final class XmrConnectionService { return numUpdates; } + public void fallbackToBestConnection() { + if (isShutDownStarted) return; + fallbackApplied = true; + if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) { + log.warn("Falling back to public nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + initializeConnections(); + } else { + log.warn("Falling back to provided nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + initializeConnections(); + if (getConnection() == null) { + log.warn("No provided nodes available, falling back to public nodes"); + fallbackToBestConnection(); + } + } + } + // ------------------------------- HELPERS -------------------------------- private void doneDownload() { @@ -460,6 +520,13 @@ public final class XmrConnectionService { private void initialize() { + // initialize key image poller + getKeyImagePoller(); + new Thread(() -> { + HavenoUtils.waitFor(20000); + keyImagePoller.poll(); // TODO: keep or remove first poll?s + }).start(); + // initialize connections initializeConnections(); @@ -526,8 +593,13 @@ public final class XmrConnectionService { // update connection if (isConnected) { setConnection(connection.getUri()); + + // reset error connecting to local node + if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) { + connectionServiceFallbackType.set(null); + } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); // switch to best connection } } @@ -535,7 +607,7 @@ public final class XmrConnectionService { } // restore connections - if ("".equals(config.xmrNode)) { + if (!isFixedConnection()) { // load previous or default connections if (coreContext.isApiUser()) { @@ -547,8 +619,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()); @@ -560,8 +634,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()); @@ -571,15 +647,17 @@ public final class XmrConnectionService { } // restore last connection - if (isFixedConnection()) { - if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size()); - connectionManager.setConnection(getConnections().get(0)); - } else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { + if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) { connectionManager.setConnection(connectionList.getCurrentConnectionUri().get()); } } + // set if last node was locally syncing + if (!isInitialized) { + usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain(); + } + // set connection proxies log.info("TOR proxy URI: " + getProxyUri()); for (MoneroRpcConnection connection : connectionManager.getConnections()) { @@ -590,12 +668,9 @@ public final class XmrConnectionService { if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui - // start local node if applicable - maybeStartLocalNode(); - // update connection - if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { - MoneroRpcConnection bestConnection = getBestAvailableConnection(); + if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { + MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); } } else if (!isInitialized) { @@ -605,9 +680,6 @@ public final class XmrConnectionService { MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1); if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); connectionManager.setConnection(connection); - - // start local node if applicable - maybeStartLocalNode(); } // register connection listener @@ -616,30 +688,26 @@ public final class XmrConnectionService { } // notify initial connection + lastRefreshPeriodMs = getRefreshPeriodMs(); onConnectionChanged(connectionManager.getConnection()); } - private void maybeStartLocalNode() { - - // skip if seed node - if (HavenoUtils.isSeedNode()) return; - - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - } + public void startLocalNode() throws Exception { + + // cannot start local node as seed node + if (HavenoUtils.isSeedNode()) { + throw new RuntimeException("Cannot start local node on seed node"); } + + // start local node + log.info("Starting local node"); + xmrLocalNode.start(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted || !accountService.isAccountOpen()) return; if (currentConnection == null) { - log.warn("Setting daemon connection to null"); - Thread.dumpStack(); + log.warn("Setting daemon connection to null", new Throwable("Stack trace")); } synchronized (lock) { if (currentConnection == null) { @@ -660,6 +728,10 @@ public final class XmrConnectionService { numUpdates.set(numUpdates.get() + 1); }); } + + // update key image poller + keyImagePoller.setDaemon(getDaemon()); + keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); // update polling doPollDaemon(); @@ -709,25 +781,31 @@ public final class XmrConnectionService { try { // poll daemon - if (daemon == null) switchToBestConnection(); - if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); + if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection(); try { + if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); lastInfo = daemon.getInfo(); } catch (Exception e) { // skip handling if shutting down if (isShutDownStarted) return; - // fallback to provided or public nodes if custom connection fails on startup - if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) { - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); - } else { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + // invoke fallback handling on startup error + boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup; + if (lastInfo == null && canFallback) { + if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + lastFallbackInvocation = System.currentTimeMillis(); + if (usedSyncingLocalNodeBeforeStartup) { + log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL); + } else if (isProvidedConnections()) { + log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED); + } else { + log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM); + } } - initializeConnections(); return; } @@ -740,11 +818,13 @@ public final class XmrConnectionService { // switch to best connection switchToBestConnection(); + if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling"); lastInfo = daemon.getInfo(); // caught internally if still fails } // connected to daemon isConnected = true; + connectionServiceFallbackType.set(null); // determine if blockchain is syncing locally boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 @@ -782,16 +862,15 @@ public final class XmrConnectionService { downloadListener.progress(percent, blocksLeft, null); } - // set peer connections - // TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections - // try { - // peers.set(getOnlinePeers()); - // } catch (Exception err) { - // // TODO: peers unknown due to restricted RPC call - // } - // numPeers.set(peers.get().size()); - numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); - peers.set(new ArrayList()); + // set available connections + List availableConnections = new ArrayList<>(); + for (MoneroRpcConnection connection : connectionManager.getConnections()) { + if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) { + availableConnections.add(connection); + } + } + connections.set(availableConnections); + numConnections.set(availableConnections.size()); // notify update numUpdates.set(numUpdates.get() + 1); @@ -821,13 +900,15 @@ public final class XmrConnectionService { } } - private List getOnlinePeers() { - return daemon.getPeers().stream() - .filter(peer -> peer.isOnline()) - .collect(Collectors.toList()); + private boolean isFixedConnection() { + return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } - private boolean isFixedConnection() { - return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + 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 cd5ed266f1..0928340d25 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; +import haveno.core.xmr.nodes.XmrNodes.XmrNode; +import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; @@ -55,6 +57,7 @@ public class XmrLocalNode { private MoneroConnectionManager connectionManager; private final Config config; private final Preferences preferences; + private final XmrNodes xmrNodes; private final List listeners = new ArrayList<>(); // required arguments @@ -69,9 +72,12 @@ public class XmrLocalNode { } @Inject - public XmrLocalNode(Config config, Preferences preferences) { + public XmrLocalNode(Config config, + Preferences preferences, + XmrNodes xmrNodes) { this.config = config; this.preferences = preferences; + this.xmrNodes = xmrNodes; this.daemon = new MoneroDaemonRpc(getUri()); // initialize connection manager to listen to local connection @@ -101,7 +107,20 @@ public class XmrLocalNode { * Returns whether Haveno should ignore a local Monero node even if it is usable. */ public boolean shouldBeIgnored() { - return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + if (config.ignoreLocalXmrNode) return true; + + // ignore if fixed connection is not local + if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode); + + // check if local node is within configuration + boolean hasConfiguredLocalNode = false; + for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { + if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) { + hasConfiguredLocalNode = true; + break; + } + } + return !hasConfiguredLocalNode; } public void addListener(XmrLocalNodeListener listener) { @@ -120,7 +139,11 @@ public class XmrLocalNode { } public boolean equalsUri(String uri) { - return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + try { + return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + } catch (Exception e) { + return false; + } } /** @@ -166,11 +189,18 @@ public class XmrLocalNode { var args = new ArrayList<>(MONEROD_ARGS); - var dataDir = settings.getBlockchainPath(); - if (dataDir == null || dataDir.isEmpty()) { - dataDir = MONEROD_DATADIR; + var dataDir = ""; + if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) { + dataDir = settings.getBlockchainPath(); + if (dataDir == null || dataDir.isEmpty()) { + dataDir = MONEROD_DATADIR; + } + } else { + dataDir = config.xmrBlockchainPath; // startup config overrides settings + } + if (dataDir != null && !dataDir.isEmpty()) { + args.add("--data-dir=" + dataDir); } - if (dataDir != null) args.add("--data-dir=" + dataDir); var bootstrapUrl = settings.getBootstrapUrl(); if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) { 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 b489aaa8bb..76de24401a 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -78,6 +78,9 @@ public class OfferInfo implements Payload { @Nullable private final String splitOutputTxHash; private final long splitOutputTxFee; + private final boolean isPrivateOffer; + private final String challenge; + private final String extraInfo; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); @@ -111,6 +114,9 @@ public class OfferInfo implements Payload { this.arbitratorSigner = builder.getArbitratorSigner(); this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxFee = builder.getSplitOutputTxFee(); + this.isPrivateOffer = builder.isPrivateOffer(); + this.challenge = builder.getChallenge(); + this.extraInfo = builder.getExtraInfo(); } public static OfferInfo toOfferInfo(Offer offer) { @@ -137,6 +143,7 @@ public class OfferInfo implements Payload { .withIsActivated(isActivated) .withSplitOutputTxHash(openOffer.getSplitOutputTxHash()) .withSplitOutputTxFee(openOffer.getSplitOutputTxFee()) + .withChallenge(openOffer.getChallenge()) .build(); } @@ -177,7 +184,10 @@ public class OfferInfo implements Payload { .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withVersionNumber(offer.getOfferPayload().getVersionNr()) .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) - .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()); + .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) + .withIsPrivateOffer(offer.isPrivateOffer()) + .withChallenge(offer.getChallenge()) + .withExtraInfo(offer.getCombinedExtraInfo()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -215,9 +225,12 @@ public class OfferInfo implements Payload { .setPubKeyRing(pubKeyRing) .setVersionNr(versionNumber) .setProtocolVersion(protocolVersion) - .setSplitOutputTxFee(splitOutputTxFee); + .setSplitOutputTxFee(splitOutputTxFee) + .setIsPrivateOffer(isPrivateOffer); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); + Optional.ofNullable(challenge).ifPresent(builder::setChallenge); + Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); return builder.build(); } @@ -255,6 +268,9 @@ public class OfferInfo implements Payload { .withArbitratorSigner(proto.getArbitratorSigner()) .withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxFee(proto.getSplitOutputTxFee()) + .withIsPrivateOffer(proto.getIsPrivateOffer()) + .withChallenge(proto.getChallenge()) + .withExtraInfo(proto.getExtraInfo()) .build(); } } 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 6650265fbb..6b1b494047 100644 --- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java +++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java @@ -77,7 +77,8 @@ public final class PaymentAccountForm implements PersistablePayload { AUSTRALIA_PAYID, CASH_APP, PAYPAL, - VENMO; + VENMO, + PAYSAFE; 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 fa94fd27f2..8df26368ba 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -172,14 +172,14 @@ public class TradeInfo implements Payload { .withAmount(trade.getAmount().longValueExact()) .withMakerFee(trade.getMakerFee().longValueExact()) .withTakerFee(trade.getTakerFee().longValueExact()) - .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact()) - .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact()) - .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact()) - .withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact()) - .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact()) - .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact()) - .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact()) - .withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact()) + .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact()) + .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact()) + .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact()) + .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact()) + .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact()) + .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact()) + .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact()) + .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact()) .withTotalTxFee(trade.getTotalTxFee().longValueExact()) .withPrice(toPreciseTradePrice.apply(trade)) .withVolume(toRoundedVolume.apply(trade)) diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java index 35d532f67f..23e403fcd2 100644 --- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java +++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java @@ -63,6 +63,9 @@ public final class OfferInfoBuilder { private String arbitratorSigner; private String splitOutputTxHash; private long splitOutputTxFee; + private boolean isPrivateOffer; + private String challenge; + private String extraInfo; public OfferInfoBuilder withId(String id) { this.id = id; @@ -234,6 +237,21 @@ public final class OfferInfoBuilder { return this; } + public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) { + this.isPrivateOffer = isPrivateOffer; + return this; + } + + public OfferInfoBuilder withChallenge(String challenge) { + this.challenge = challenge; + return this; + } + + public OfferInfoBuilder withExtraInfo(String extraInfo) { + this.extraInfo = extraInfo; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/core/src/main/java/haveno/core/app/AppStartupState.java b/core/src/main/java/haveno/core/app/AppStartupState.java index 85da2af9d4..89fe61576a 100644 --- a/core/src/main/java/haveno/core/app/AppStartupState.java +++ b/core/src/main/java/haveno/core/app/AppStartupState.java @@ -73,7 +73,7 @@ public class AppStartupState { isWalletSynced.set(true); }); - xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> { + xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> { if (xmrConnectionService.hasSufficientPeersForBroadcast()) hasSufficientPeersForBroadcast.set(true); }); diff --git a/core/src/main/java/haveno/core/app/DomainInitialisation.java b/core/src/main/java/haveno/core/app/DomainInitialisation.java index 646d80d9dd..220fb05040 100644 --- a/core/src/main/java/haveno/core/app/DomainInitialisation.java +++ b/core/src/main/java/haveno/core/app/DomainInitialisation.java @@ -178,6 +178,9 @@ public class DomainInitialisation { closedTradableManager.onAllServicesInitialized(); failedTradesManager.onAllServicesInitialized(); + filterManager.setFilterWarningHandler(filterWarningHandler); + filterManager.onAllServicesInitialized(); + openOfferManager.onAllServicesInitialized(); balances.onAllServicesInitialized(); @@ -199,10 +202,6 @@ public class DomainInitialisation { priceFeedService.setCurrencyCodeOnInit(); priceFeedService.startRequestingPrices(); - filterManager.setFilterWarningHandler(filterWarningHandler); - filterManager.onAllServicesInitialized(); - - mobileNotificationService.onAllServicesInitialized(); myOfferTakenEvents.onAllServicesInitialized(); tradeEvents.onAllServicesInitialized(); diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java index 5f2d14622b..8f2998eb0d 100644 --- a/core/src/main/java/haveno/core/app/HavenoExecutable.java +++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java @@ -100,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven protected AppModule module; protected Config config; @Getter - protected boolean isShutdownInProgress; + protected boolean isShutDownStarted; private boolean isReadOnly; private Thread keepRunningThread; private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS); @@ -330,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); - // ignore if shut down in progress - if (isShutdownInProgress) { - log.info("Ignoring call to gracefulShutDown, already in progress"); + // ignore if shut down started + if (isShutDownStarted) { + log.info("Ignoring call to gracefulShutDown, already started"); return; } - isShutdownInProgress = true; + isShutDownStarted = true; ResultHandler resultHandler; if (shutdownCompletedHandler != null) { @@ -357,45 +357,46 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven // notify trade protocols and wallets to prepare for shut down before shutting down Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); - tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout } catch (Exception e) { log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e); } - injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager - log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); + log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { - // shut down offer book service - injector.getInstance(OfferBookService.class).shutDown(); + // listen for shut down of wallets setup + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // shut down p2p service - injector.getInstance(P2PService.class).shutDown(() -> { + // shut down p2p service + log.info("Shutting down P2P service"); + injector.getInstance(P2PService.class).shutDown(() -> { - // shut down monero wallets and connections - log.info("Shutting down wallet and connection services"); - injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // done shutting down log.info("Graceful shutdown completed. Exiting now."); module.close(injector); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit); }); - injector.getInstance(BtcWalletService.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(); - injector.getInstance(XmrConnectionService.class).shutDown(); - injector.getInstance(WalletsSetup.class).shutDown(); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(XmrConnectionService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); } catch (Throwable t) { log.error("App shutdown failed with exception: {}\n", t.getMessage(), t); diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 7235efce7b..0bdac1abc1 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,6 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); @@ -85,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp { havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler")); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index d80ca807cc..192e3870b7 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; +import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -158,6 +159,9 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable + private Consumer displayMoneroConnectionFallbackHandler; + @Setter + @Nullable private Consumer displayTorNetworkSettingsHandler; @Setter @Nullable @@ -173,7 +177,7 @@ public class HavenoSetup { private Consumer displayPrivateNotificationHandler; @Setter @Nullable - private Runnable showPopupIfInvalidBtcConfigHandler; + private Runnable showPopupIfInvalidXmrConfigHandler; @Setter @Nullable private Consumer> revolutAccountsUpdateHandler; @@ -366,7 +370,7 @@ public class HavenoSetup { // install monerod File monerodFile = new File(XmrLocalNode.MONEROD_PATH); String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME; - if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) { + if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) { log.info("Installing monerod"); monerodFile.getParentFile().mkdirs(); FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile); @@ -376,7 +380,7 @@ public class HavenoSetup { // install monero-wallet-rpc File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH); String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME; - if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) { + if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) { log.info("Installing monero-wallet-rpc"); moneroWalletRpcFile.getParentFile().mkdirs(); FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile); @@ -426,6 +430,12 @@ public class HavenoSetup { getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); + // listen for fallback handling + getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); + }); + log.info("Init P2P network"); havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); @@ -452,7 +462,7 @@ public class HavenoSetup { havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, - showPopupIfInvalidBtcConfigHandler, + showPopupIfInvalidXmrConfigHandler, () -> {}, () -> {}); } @@ -725,6 +735,10 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } + public ObjectProperty getConnectionServiceFallbackType() { + return xmrConnectionService.getConnectionServiceFallbackType(); + } + public StringProperty getTopErrorMsg() { return topErrorMsg; } diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java index 4e90faeb55..69609a4bba 100644 --- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java @@ -87,7 +87,7 @@ public class P2PNetworkSetup { BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), - xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, + xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived, (state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> { String result; int p2pPeers = (int) numP2pPeers; diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 1f7946eac7..7d37372afd 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -117,10 +117,10 @@ public class WalletAppSetup { void init(@Nullable Consumer chainFileLockedExceptionHandler, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, - @Nullable Runnable showPopupIfInvalidBtcConfigHandler, + @Nullable Runnable showPopupIfInvalidXmrConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { - log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion()); + log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); xmrInfoBinding = EasyBind.combine( @@ -199,8 +199,8 @@ public class WalletAppSetup { walletInitializedHandler.run(); }, exception -> { - if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { - showPopupIfInvalidBtcConfigHandler.run(); + if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) { + showPopupIfInvalidXmrConfigHandler.run(); } else { walletServiceException.set(exception); } 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 725ccd877c..3184d9ba11 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -105,21 +105,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { public void gracefulShutDown(ResultHandler resultHandler) { log.info("Starting graceful shut down of {}", getClass().getSimpleName()); - // ignore if shut down in progress - if (isShutdownInProgress) { - log.info("Ignoring call to gracefulShutDown, already in progress"); + // ignore if shut down started + if (isShutDownStarted) { + log.info("Ignoring call to gracefulShutDown, already started"); return; } - isShutdownInProgress = true; + isShutDownStarted = true; try { if (injector != null) { // notify trade protocols and wallets to prepare for shut down Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted()); - tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted()); try { ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout } catch (Exception e) { @@ -127,25 +127,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { } JsonFileManager.shutDownAllInstances(); - injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); // shut down open offer manager - log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); + log.info("Shutting down OpenOfferManager"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { - // shut down offer book service - injector.getInstance(OfferBookService.class).shutDown(); + // listen for shut down of wallets setup + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - // shut down p2p service - injector.getInstance(P2PService.class).shutDown(() -> { - - // shut down monero wallets and connections - log.info("Shutting down wallet and connection services"); - injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { + // shut down p2p service + log.info("Shutting down P2P service"); + injector.getInstance(P2PService.class).shutDown(() -> { module.close(injector); PersistenceManager.flushAllDataToDiskAtShutdown(() -> { @@ -155,18 +151,23 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }); - injector.getInstance(BtcWalletService.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(); - injector.getInstance(XmrConnectionService.class).shutDown(); - injector.getInstance(WalletsSetup.class).shutDown(); }); + + // shut down trade and wallet services + log.info("Shutting down trade and wallet services"); + injector.getInstance(OfferBookService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(XmrConnectionService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); // we wait max 5 sec. UserThread.runAfter(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); - log.info("Graceful shutdown caused a timeout. Exiting now."); + log.warn("Graceful shutdown caused a timeout. Exiting now."); UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); }); }, 5); diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java index cb7e0e9b21..2cebb66f3a 100644 --- a/core/src/main/java/haveno/core/filter/FilterManager.java +++ b/core/src/main/java/haveno/core/filter/FilterManager.java @@ -406,6 +406,10 @@ public class FilterManager { .anyMatch(e -> e.equals(address)); } + public String getDisableTradeBelowVersion() { + return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion(); + } + public boolean requireUpdateToNewVersionForTrading() { if (getFilter() == null) { return false; diff --git a/core/src/main/java/haveno/core/locale/CryptoCurrency.java b/core/src/main/java/haveno/core/locale/CryptoCurrency.java index bbc5b4a4be..6c46c9d2b3 100644 --- a/core/src/main/java/haveno/core/locale/CryptoCurrency.java +++ b/core/src/main/java/haveno/core/locale/CryptoCurrency.java @@ -19,10 +19,8 @@ package haveno.core.locale; import com.google.protobuf.Message; -import lombok.EqualsAndHashCode; import lombok.Getter; -@EqualsAndHashCode(callSuper = true) public final class CryptoCurrency extends TradeCurrency { // http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618 private final static String PREFIX = "✦ "; diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index b482ea4f28..6ed42b6234 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -73,14 +73,6 @@ public class CurrencyUtil { private static String baseCurrencyCode = "XMR"; - private static List getTraditionalNonFiatCurrencies() { - return Arrays.asList( - new TraditionalCurrency("XAG", "Silver"), - new TraditionalCurrency("XAU", "Gold"), - new TraditionalCurrency("XGB", "Goldback") - ); - } - // Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results. // The main improvement was already achieved with using memoize for the source maps, but // the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50% @@ -124,6 +116,14 @@ public class CurrencyUtil { return new ArrayList<>(traditionalCurrencyMapSupplier.get().values()); } + public static List getTraditionalNonFiatCurrencies() { + return Arrays.asList( + new TraditionalCurrency("XAG", "Silver"), + new TraditionalCurrency("XAU", "Gold"), + new TraditionalCurrency("XGB", "Goldback") + ); + } + public static Collection getAllSortedTraditionalCurrencies(Comparator comparator) { return (List) getAllSortedTraditionalCurrencies().stream() .sorted(comparator) @@ -200,6 +200,10 @@ public class CurrencyUtil { result.add(new CryptoCurrency("BCH", "Bitcoin Cash")); result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); + result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)")); + result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)")); + result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)")); + result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)")); result.sort(TradeCurrency::compareTo); return result; } @@ -295,6 +299,9 @@ public class CurrencyUtil { if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) { return isCryptoCurrencyMap.get(currencyCode.toUpperCase()); } + if (isCryptoCurrencyCodeBase(currencyCode)) { + return true; + } boolean isCryptoCurrency; if (currencyCode == null) { @@ -321,6 +328,21 @@ public class CurrencyUtil { return isCryptoCurrency; } + private static boolean isCryptoCurrencyCodeBase(String currencyCode) { + if (currencyCode == null) return false; + currencyCode = currencyCode.toUpperCase(); + return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI"); + } + + public static String getCurrencyCodeBase(String currencyCode) { + if (currencyCode == null) return null; + currencyCode = currencyCode.toUpperCase(); + if (currencyCode.contains("USDT")) return "USDT"; + if (currencyCode.contains("USDC")) return "USDC"; + if (currencyCode.contains("DAI")) return "DAI"; + return currencyCode; + } + public static Optional getCryptoCurrency(String currencyCode) { return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode)); } diff --git a/core/src/main/java/haveno/core/locale/TradeCurrency.java b/core/src/main/java/haveno/core/locale/TradeCurrency.java index a9da96ea16..b60abf5e88 100644 --- a/core/src/main/java/haveno/core/locale/TradeCurrency.java +++ b/core/src/main/java/haveno/core/locale/TradeCurrency.java @@ -19,19 +19,16 @@ package haveno.core.locale; import haveno.common.proto.ProtobufferRuntimeException; import haveno.common.proto.persistable.PersistablePayload; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -@EqualsAndHashCode @ToString @Getter @Slf4j public abstract class TradeCurrency implements PersistablePayload, Comparable { protected final String code; - @EqualsAndHashCode.Exclude protected final String name; public TradeCurrency(String code, String name) { @@ -82,4 +79,23 @@ public abstract class TradeCurrency implements PersistablePayload, Comparable acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); - - // reserved for future use cases - // Use null values if not set - boolean isPrivateOffer = false; + boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit; + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit); boolean useAutoClose = false; boolean useReOpenAfterAutoClose = false; long lowerClosePrice = 0; long upperClosePrice = 0; - String hashOfChallenge = null; - Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, - currencyCode, - direction); + Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction); offerUtil.validateOfferData( - securityDepositAsDouble, + securityDepositPct, paymentAccount, currencyCode); @@ -189,11 +204,11 @@ public class CreateOfferService { useMarketBasedPriceValue, amountAsLong, minAmountAsLong, - HavenoUtils.MAKER_FEE_PCT, - HavenoUtils.TAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT, + hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT, - securityDepositAsDouble, - securityDepositAsDouble, + hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers + securityDepositPct, baseCurrencyCode, counterCurrencyCode, paymentAccount.getPaymentMethod().getId(), @@ -211,44 +226,110 @@ public class CreateOfferService { upperClosePrice, lowerClosePrice, isPrivateOffer, - hashOfChallenge, + challengeHash, extraDataMap, Version.TRADE_PROTOCOL_VERSION, null, null, - null); + null, + extraInfo); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); + offer.setChallenge(challenge); return offer; } - public BigInteger getReservedFundsForOffer(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { + // TODO: add trigger price? + public Offer createClonedOffer(Offer sourceOffer, + String currencyCode, + Price fixedPrice, + boolean useMarketBasedPrice, + double marketPriceMargin, + PaymentAccount paymentAccount, + String extraInfo) { + log.info("Cloning offer with sourceId={}, " + + "currencyCode={}, " + + "fixedPrice={}, " + + "useMarketBasedPrice={}, " + + "marketPriceMargin={}, " + + "extraInfo={}", + sourceOffer.getId(), + currencyCode, + fixedPrice == null ? null : fixedPrice.getValue(), + useMarketBasedPrice, + marketPriceMargin, + extraInfo); - BigInteger reservedFundsForOffer = getSecurityDeposit(direction, - amount, - buyerSecurityDeposit, - sellerSecurityDeposit); - if (!offerUtil.isBuyOffer(direction)) - reservedFundsForOffer = reservedFundsForOffer.add(amount); + OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload(); + String newOfferId = OfferUtil.getRandomOfferId(); + Offer editedOffer = createAndGetOffer(newOfferId, + sourceOfferPayload.getDirection(), + currencyCode, + BigInteger.valueOf(sourceOfferPayload.getAmount()), + BigInteger.valueOf(sourceOfferPayload.getMinAmount()), + fixedPrice, + useMarketBasedPrice, + marketPriceMargin, + sourceOfferPayload.getSellerSecurityDepositPct(), + paymentAccount, + sourceOfferPayload.isPrivateOffer(), + sourceOfferPayload.isBuyerAsTakerWithoutDeposit(), + extraInfo); - return reservedFundsForOffer; - } - - public BigInteger getSecurityDeposit(OfferDirection direction, - BigInteger amount, - double buyerSecurityDeposit, - double sellerSecurityDeposit) { - return offerUtil.isBuyOffer(direction) ? - getBuyerSecurityDeposit(amount, buyerSecurityDeposit) : - getSellerSecurityDeposit(amount, sellerSecurityDeposit); - } - - public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit : - Restrictions.getSellerSecurityDepositAsPercent(); + // generate one-time challenge for private offer + String challenge = null; + String challengeHash = null; + if (sourceOfferPayload.isPrivateOffer()) { + challenge = HavenoUtils.generateChallenge(); + challengeHash = HavenoUtils.getChallengeHash(challenge); + } + + OfferPayload editedOfferPayload = editedOffer.getOfferPayload(); + long date = new Date().getTime(); + OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, + date, + sourceOfferPayload.getOwnerNodeAddress(), + sourceOfferPayload.getPubKeyRing(), + sourceOfferPayload.getDirection(), + editedOfferPayload.getPrice(), + editedOfferPayload.getMarketPriceMarginPct(), + editedOfferPayload.isUseMarketBasedPrice(), + sourceOfferPayload.getAmount(), + sourceOfferPayload.getMinAmount(), + sourceOfferPayload.getMakerFeePct(), + sourceOfferPayload.getTakerFeePct(), + sourceOfferPayload.getPenaltyFeePct(), + sourceOfferPayload.getBuyerSecurityDepositPct(), + sourceOfferPayload.getSellerSecurityDepositPct(), + editedOfferPayload.getBaseCurrencyCode(), + editedOfferPayload.getCounterCurrencyCode(), + editedOfferPayload.getPaymentMethodId(), + editedOfferPayload.getMakerPaymentAccountId(), + editedOfferPayload.getCountryCode(), + editedOfferPayload.getAcceptedCountryCodes(), + editedOfferPayload.getBankId(), + editedOfferPayload.getAcceptedBankIds(), + editedOfferPayload.getVersionNr(), + sourceOfferPayload.getBlockHeightAtOfferCreation(), + editedOfferPayload.getMaxTradeLimit(), + editedOfferPayload.getMaxTradePeriod(), + sourceOfferPayload.isUseAutoClose(), + sourceOfferPayload.isUseReOpenAfterAutoClose(), + sourceOfferPayload.getLowerClosePrice(), + sourceOfferPayload.getUpperClosePrice(), + sourceOfferPayload.isPrivateOffer(), + challengeHash, + editedOfferPayload.getExtraDataMap(), + sourceOfferPayload.getProtocolVersion(), + null, + null, + sourceOfferPayload.getReserveTxKeyImages(), + editedOfferPayload.getExtraInfo()); + Offer clonedOffer = new Offer(clonedOfferPayload); + clonedOffer.setPriceFeedService(priceFeedService); + clonedOffer.setChallenge(challenge); + clonedOffer.setState(Offer.State.AVAILABLE); + return clonedOffer; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -259,26 +340,4 @@ public class CreateOfferService { MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } - - private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); - } - - private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); - } - - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); - } } diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 675fbeb550..8df8511b3a 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -18,7 +18,6 @@ package haveno.core.offer; import haveno.common.ThreadUtils; -import haveno.common.UserThread; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; @@ -115,6 +114,12 @@ public class Offer implements NetworkPayload, PersistablePayload { @Setter transient private boolean isReservedFundsSpent; + @JsonExclude + @Getter + @Setter + @Nullable + transient private String challenge; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -275,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload { } public void setErrorMessage(String errorMessage) { - UserThread.await(() -> errorMessageProperty.set(errorMessage)); + errorMessageProperty.set(errorMessage); } @@ -337,6 +342,18 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.getSellerSecurityDepositPct(); } + public boolean isPrivateOffer() { + return offerPayload.isPrivateOffer(); + } + + public String getChallengeHash() { + return offerPayload.getChallengeHash(); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; + } + public BigInteger getMaxTradeLimit() { return BigInteger.valueOf(offerPayload.getMaxTradeLimit()); } @@ -403,7 +420,23 @@ public class Offer implements NetworkPayload, PersistablePayload { return ""; } - public String getExtraInfo() { + public String getCombinedExtraInfo() { + StringBuilder sb = new StringBuilder(); + if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) { + sb.append(getOfferExtraInfo()); + } + if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append(getPaymentAccountExtraInfo()); + } + return sb.toString(); + } + + public String getOfferExtraInfo() { + return offerPayload.getExtraInfo(); + } + + public String getPaymentAccountExtraInfo() { if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO)) @@ -414,6 +447,8 @@ public class Offer implements NetworkPayload, PersistablePayload { return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO); else return ""; } diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 7698aeb1ca..50981a8fa6 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -36,6 +36,9 @@ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.ThreadUtils; +import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; @@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; -import haveno.core.trade.HavenoUtils; import haveno.core.util.JsonUtil; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrKeyImageListener; -import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.P2PService; import haveno.network.p2p.storage.HashMapChangedListener; import haveno.network.p2p.storage.payload.ProtectedStorageEntry; +import haveno.network.utils.Utils; +import lombok.extern.slf4j.Slf4j; + import java.io.File; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import monero.daemon.model.MoneroKeyImageSpentStatus; /** - * Handles storage and retrieval of offers. - * Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer). + * Handles validation and announcement of offers added or removed. */ +@Slf4j public class OfferBookService { + private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes + private final P2PService p2PService; private final PriceFeedService priceFeedService; private final List offerBookChangedListeners = new LinkedList<>(); private final FilterManager filterManager; private final JsonFileManager jsonFileManager; private final XmrConnectionService xmrConnectionService; - - // poll key images of offers - private XmrKeyImagePoller keyImagePoller; - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds - private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes + private final List validOffers = new ArrayList(); + private final List invalidOffers = new ArrayList(); + private final Map invalidOfferTimers = new HashMap<>(); public interface OfferBookChangedListener { void onAdded(Offer offer); - void onRemoved(Offer offer); } @@ -104,51 +113,59 @@ public class OfferBookService { this.xmrConnectionService = xmrConnectionService; jsonFileManager = new JsonFileManager(storageDir); - // listen for connection changes to monerod - xmrConnectionService.addConnectionListener((connection) -> { - maybeInitializeKeyImagePoller(); - keyImagePoller.setDaemon(xmrConnectionService.getDaemon()); - keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); - }); - // listen for offers p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { - UserThread.execute(() -> { + ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); + synchronized (validOffers) { + try { + validateOfferPayload(offerPayload); + replaceValidOffer(offer); + announceOfferAdded(offer); + } catch (IllegalArgumentException e) { + // ignore illegal offers + } catch (RuntimeException e) { + replaceInvalidOffer(offer); // offer can become valid later + } } } }); - }); + }, OfferBookService.class.getSimpleName()); } @Override public void onRemoved(Collection protectedStorageEntries) { - UserThread.execute(() -> { + ThreadUtils.execute(() -> { protectedStorageEntries.forEach(protectedStorageEntry -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); + removeValidOffer(offerPayload.getId()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); + announceOfferRemoved(offer); + + // check if invalid offers are now valid + synchronized (invalidOffers) { + for (Offer invalidOffer : new ArrayList(invalidOffers)) { + try { + validateOfferPayload(invalidOffer.getOfferPayload()); + removeInvalidOffer(invalidOffer.getId()); + replaceValidOffer(invalidOffer); + announceOfferAdded(invalidOffer); + } catch (Exception e) { + // ignore + } + } } } }); - }); + }, OfferBookService.class.getSimpleName()); } }); @@ -171,6 +188,16 @@ public class OfferBookService { } }); } + + // listen for changes to key images + xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (String keyImage : spentStatuses.keySet()) { + updateAffectedOffers(keyImage); + } + } + }); } @@ -178,6 +205,10 @@ public class OfferBookService { // API /////////////////////////////////////////////////////////////////////////////////////////// + public boolean hasOffer(String offerId) { + return hasValidOffer(offerId); + } + public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (filterManager.requireUpdateToNewVersionForTrading()) { errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); @@ -233,16 +264,9 @@ public class OfferBookService { } public List getOffers() { - return p2PService.getDataMap().values().stream() - .filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload) - .map(data -> { - OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload(); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - return offer; - }) - .collect(Collectors.toList()); + synchronized (validOffers) { + return new ArrayList<>(validOffers); + } } public List getOffersByCurrency(String direction, String currencyCode) { @@ -266,7 +290,7 @@ public class OfferBookService { } public void shutDown() { - if (keyImagePoller != null) keyImagePoller.clearKeyImages(); + xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName()); } @@ -274,37 +298,131 @@ public class OfferBookService { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private synchronized void maybeInitializeKeyImagePoller() { - if (keyImagePoller != null) return; - keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs()); + private void announceOfferAdded(Offer offer) { + xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName()); + updateReservedFundsSpentStatus(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); + } + } - // handle when key images spent - keyImagePoller.addListener(new XmrKeyImageListener() { - @Override - public void onSpentStatusChanged(Map spentStatuses) { - UserThread.execute(() -> { - for (String keyImage : spentStatuses.keySet()) { - updateAffectedOffers(keyImage); - } - }); + private void announceOfferRemoved(Offer offer) { + updateReservedFundsSpentStatus(offer); + removeKeyImages(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); + } + } + + private boolean hasValidOffer(String offerId) { + for (Offer offer : getOffers()) { + if (offer.getId().equals(offerId)) { + return true; } - }); - - // first poll after 20s - // TODO: remove? - new Thread(() -> { - HavenoUtils.waitFor(20000); - keyImagePoller.poll(); - }).start(); + } + return false; + } + + private void replaceValidOffer(Offer offer) { + synchronized (validOffers) { + removeValidOffer(offer.getId()); + validOffers.add(offer); + } } - private long getKeyImageRefreshPeriodMs() { - return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; + private void replaceInvalidOffer(Offer offer) { + synchronized (invalidOffers) { + removeInvalidOffer(offer.getId()); + invalidOffers.add(offer); + + // remove invalid offer after timeout + synchronized (invalidOfferTimers) { + Timer timer = invalidOfferTimers.get(offer.getId()); + if (timer != null) timer.stop(); + timer = UserThread.runAfter(() -> { + removeInvalidOffer(offer.getId()); + }, INVALID_OFFERS_TIMEOUT); + invalidOfferTimers.put(offer.getId(), timer); + } + } } + private void removeValidOffer(String offerId) { + synchronized (validOffers) { + validOffers.removeIf(offer -> offer.getId().equals(offerId)); + } + } + + private void removeInvalidOffer(String offerId) { + synchronized (invalidOffers) { + invalidOffers.removeIf(offer -> offer.getId().equals(offerId)); + + // remove timeout + synchronized (invalidOfferTimers) { + Timer timer = invalidOfferTimers.get(offerId); + if (timer != null) timer.stop(); + invalidOfferTimers.remove(offerId); + } + } + } + + private void validateOfferPayload(OfferPayload offerPayload) { + + // validate offer is not banned + if (filterManager.isOfferIdBanned(offerPayload.getId())) { + throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId()); + } + + // validate v3 node address compliance + boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName()); + if (!isV3NodeAddressCompliant) { + throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId()); + } + + // validate against existing offers + synchronized (validOffers) { + int numOffersWithSharedKeyImages = 0; + for (Offer offer : validOffers) { + + // validate that no offer has overlapping but different key images + if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) && + !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) { + throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId()); + } + + // validate that no offer has same key images, payment method, and currency + if (!offer.getId().equals(offerPayload.getId()) && + offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) && + offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) && + offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) && + offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) { + throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId()); + } + + // count offers with same key images + if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1); + } + + // validate max offers with same key images + if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId()); + } + } + + private void removeKeyImages(Offer offer) { + Set unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages()); + synchronized (validOffers) { + for (Offer validOffer : validOffers) { + if (validOffer.getId().equals(offer.getId())) continue; + unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages()); + } + } + xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName()); + } + private void updateAffectedOffers(String keyImage) { for (Offer offer : getOffers()) { if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + updateReservedFundsSpentStatus(offer); synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> { listener.onRemoved(offer); @@ -315,10 +433,9 @@ public class OfferBookService { } } - private void setReservedFundsSpent(Offer offer) { - if (keyImagePoller == null) return; + private void updateReservedFundsSpentStatus(Offer offer) { for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) { - if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) { + if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) { offer.setReservedFundsSpent(true); } } diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java index 51ac8cdee7..e64a1ee6eb 100644 --- a/core/src/main/java/haveno/core/offer/OfferFilterService.java +++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java @@ -201,7 +201,7 @@ public class OfferFilterService { accountAgeWitnessService); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection())) + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit())) .orElse(0L); long offerMinAmount = offer.getMinAmount().longValueExact(); log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index fa05685dee..1db2cca940 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; + public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker @@ -156,7 +157,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Reserved for possible future use to support private trades where the taker needs to have an accessKey private final boolean isPrivateOffer; @Nullable - private final String hashOfChallenge; + private final String challengeHash; + @Nullable + private final String extraInfo; /////////////////////////////////////////////////////////////////////////////////////////// @@ -195,12 +198,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay long lowerClosePrice, long upperClosePrice, boolean isPrivateOffer, - @Nullable String hashOfChallenge, + @Nullable String challengeHash, @Nullable Map extraDataMap, int protocolVersion, @Nullable NodeAddress arbitratorSigner, @Nullable byte[] arbitratorSignature, - @Nullable List reserveTxKeyImages) { + @Nullable List reserveTxKeyImages, + @Nullable String extraInfo) { this.id = id; this.date = date; this.ownerNodeAddress = ownerNodeAddress; @@ -238,7 +242,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay this.lowerClosePrice = lowerClosePrice; this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; - this.hashOfChallenge = hashOfChallenge; + this.challengeHash = challengeHash; + this.extraInfo = extraInfo; } public byte[] getHash() { @@ -284,12 +289,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay lowerClosePrice, upperClosePrice, isPrivateOffer, - hashOfChallenge, + challengeHash, extraDataMap, protocolVersion, arbitratorSigner, null, - reserveTxKeyImages + reserveTxKeyImages, + null ); return signee.getHash(); @@ -328,12 +334,21 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct()); - return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted); + boolean isBuyerTaker = getDirection() == OfferDirection.SELL; + if (isPrivateOffer() && isBuyerTaker) { + return securityDepositUnadjusted; + } else { + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); + } } public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) { BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct()); - return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted); + return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted); + } + + public boolean isBuyerAsTakerWithoutDeposit() { + return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -376,11 +391,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay Optional.ofNullable(bankId).ifPresent(builder::setBankId); Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); - Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); + Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage())); Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e))); Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages); + Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo); return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); } @@ -392,7 +408,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay null : new ArrayList<>(proto.getAcceptedCountryCodesList()); List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ? null : new ArrayList<>(proto.getReserveTxKeyImagesList()); - String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); @@ -428,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay proto.getLowerClosePrice(), proto.getUpperClosePrice(), proto.getIsPrivateOffer(), - hashOfChallenge, + ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()), extraDataMapMap, proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), - reserveTxKeyImages); + reserveTxKeyImages, + ProtoUtil.stringOrNullFromProto(proto.getExtraInfo())); } @Override @@ -475,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay ",\r\n lowerClosePrice=" + lowerClosePrice + ",\r\n upperClosePrice=" + upperClosePrice + ",\r\n isPrivateOffer=" + isPrivateOffer + - ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + + ",\r\n challengeHash='" + challengeHash + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + + ",\r\n extraInfo='" + extraInfo + "\r\n} "; } // For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions. - // The json is used for the hash in the contract and change of oder would cause a different hash and + // The json is used for the hash in the contract and change of order would cause a different hash and // therefore a failure during trade. public static class JsonSerializer implements com.google.gson.JsonSerializer { @Override @@ -519,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); + object.add("extraInfo", context.serialize(offerPayload.getExtraInfo())); + // reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts return object; } } diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 72593ab5e7..2e2644630a 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -37,6 +37,7 @@ import haveno.core.monetary.Volume; import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CAPABILITIES; +import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; @@ -48,6 +49,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.CashAppAccount; +import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; @@ -58,8 +60,8 @@ import haveno.core.trade.statistics.ReferralIdService; import haveno.core.user.AutoConfirmSettings; import haveno.core.user.Preferences; import haveno.core.util.coin.CoinFormatter; -import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; -import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent; import haveno.network.p2p.P2PService; import java.math.BigInteger; import java.util.HashMap; @@ -120,9 +122,10 @@ public class OfferUtil { public long getMaxTradeLimit(PaymentAccount paymentAccount, String currencyCode, - OfferDirection direction) { + OfferDirection direction, + boolean buyerAsTakerWithoutDeposit) { return paymentAccount != null - ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) + ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit) : 0; } @@ -216,6 +219,10 @@ public class OfferUtil { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } + if (paymentAccount instanceof CashAtAtmAccount) { + extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo()); + } + extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) { @@ -228,16 +235,16 @@ public class OfferUtil { return extraDataMap.isEmpty() ? null : extraDataMap; } - public void validateOfferData(double buyerSecurityDeposit, + public void validateOfferData(double securityDeposit, PaymentAccount paymentAccount, String currencyCode) { checkNotNull(p2PService.getAddress(), "Address must not be null"); - checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), + checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(), "securityDeposit must not exceed " + - getMaxBuyerSecurityDepositAsPercent()); - checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), + getMaxSecurityDepositAsPercent()); + checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(), "securityDeposit must not be less than " + - getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit); + getMinSecurityDepositAsPercent() + " but was " + securityDeposit); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index b0df3e0e35..f493b1b584 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.UUID; @EqualsAndHashCode public final class OpenOffer implements Tradable { @@ -96,6 +97,9 @@ public final class OpenOffer implements Tradable { @Getter private String reserveTxKey; @Getter + @Setter + private String challenge; + @Getter private final long triggerPrice; @Getter @Setter @@ -107,6 +111,12 @@ public final class OpenOffer implements Tradable { @Getter @Setter transient int numProcessingAttempts = 0; + @Getter + @Setter + private boolean deactivatedByTrigger; + @Getter + @Setter + private String groupId; public OpenOffer(Offer offer) { this(offer, 0, false); @@ -120,6 +130,8 @@ public final class OpenOffer implements Tradable { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; + this.challenge = offer.getChallenge(); + this.groupId = UUID.randomUUID().toString(); state = State.PENDING; } @@ -137,6 +149,9 @@ public final class OpenOffer implements Tradable { this.reserveTxHash = openOffer.reserveTxHash; this.reserveTxHex = openOffer.reserveTxHex; this.reserveTxKey = openOffer.reserveTxKey; + this.challenge = openOffer.challenge; + this.deactivatedByTrigger = openOffer.deactivatedByTrigger; + this.groupId = openOffer.groupId; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -153,7 +168,10 @@ public final class OpenOffer implements Tradable { long splitOutputTxFee, @Nullable String reserveTxHash, @Nullable String reserveTxHex, - @Nullable String reserveTxKey) { + @Nullable String reserveTxKey, + @Nullable String challenge, + boolean deactivatedByTrigger, + @Nullable String groupId) { this.offer = offer; this.state = state; this.triggerPrice = triggerPrice; @@ -164,6 +182,10 @@ public final class OpenOffer implements Tradable { this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; + this.challenge = challenge; + this.deactivatedByTrigger = deactivatedByTrigger; + if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19) + this.groupId = groupId; // reset reserved state to available if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -176,7 +198,8 @@ public final class OpenOffer implements Tradable { .setTriggerPrice(triggerPrice) .setState(protobuf.OpenOffer.State.valueOf(state.name())) .setSplitOutputTxFee(splitOutputTxFee) - .setReserveExactAmount(reserveExactAmount); + .setReserveExactAmount(reserveExactAmount) + .setDeactivatedByTrigger(deactivatedByTrigger); Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount)); Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes)); @@ -184,6 +207,8 @@ public final class OpenOffer implements Tradable { Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); + Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId)); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -199,7 +224,10 @@ public final class OpenOffer implements Tradable { proto.getSplitOutputTxFee(), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), - ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey())); + ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), + ProtoUtil.stringOrNullFromProto(proto.getChallenge()), + proto.getDeactivatedByTrigger(), + ProtoUtil.stringOrNullFromProto(proto.getGroupId())); return openOffer; } @@ -226,6 +254,14 @@ public final class OpenOffer implements Tradable { public void setState(State state) { this.state = state; stateProperty.set(state); + if (state == State.AVAILABLE) { + deactivatedByTrigger = false; + } + } + + public void deactivate(boolean deactivatedByTrigger) { + this.deactivatedByTrigger = deactivatedByTrigger; + setState(State.DEACTIVATED); } public ReadOnlyObjectProperty stateProperty() { @@ -257,6 +293,7 @@ public final class OpenOffer implements Tradable { ",\n reserveExactAmount=" + reserveExactAmount + ",\n scheduledAmount=" + scheduledAmount + ",\n splitOutputTxFee=" + splitOutputTxFee + + ",\n groupId=" + groupId + "\n}"; } } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index ffed498bc9..a928494c1d 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -35,6 +35,8 @@ package haveno.core.offer; import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; import com.google.inject.Inject; import haveno.common.ThreadUtils; import haveno.common.Timer; @@ -55,13 +57,14 @@ import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.filter.FilterManager; -import haveno.core.offer.OfferBookService.OfferBookChangedListener; +import haveno.core.locale.Res; import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.SignOfferRequest; import haveno.core.offer.messages.SignOfferResponse; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferProtocol; +import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.provider.price.PriceFeedService; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; @@ -78,6 +81,7 @@ import haveno.core.util.JsonUtil; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.BtcWalletService; +import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.core.xmr.wallet.TradeWalletService; @@ -113,7 +117,6 @@ import lombok.Getter; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; -import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTransferQuery; @@ -133,7 +136,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; - private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts + private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts + private static final long SHUTDOWN_TIMEOUT_MS = 60000; + private static final String OPEN_OFFER_GROUP_KEY_IMAGE_ID = OpenOffer.class.getSimpleName(); + private static final String SIGNED_OFFER_KEY_IMAGE_GROUP_ID = SignedOffer.class.getSimpleName(); private final CoreContext coreContext; private final KeyRing keyRing; @@ -167,12 +173,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Getter private final AccountAgeWitnessService accountAgeWitnessService; - // poll key images of signed offers - private XmrKeyImagePoller signedOfferKeyImagePoller; - private static final long SHUTDOWN_TIMEOUT_MS = 60000; - 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 Object processOffersLock = new Object(); // lock for processing offers @@ -225,27 +225,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE); this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers - - // listen for connection changes to monerod - xmrConnectionService.addConnectionListener((connection) -> maybeInitializeKeyImagePoller()); - - // close open offer if reserved funds spent - offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() { - @Override - public void onAdded(Offer offer) { - - // cancel offer if reserved funds spent - Optional openOfferOptional = getOpenOfferById(offer.getId()); - if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) { - log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState()); - cancelOpenOffer(openOfferOptional.get(), null, null); - } - } - @Override - public void onRemoved(Offer offer) { - // nothing to do - } - }); } @Override @@ -266,34 +245,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe completeHandler); } - private synchronized void maybeInitializeKeyImagePoller() { - if (signedOfferKeyImagePoller != null) return; - signedOfferKeyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs()); - - // handle when key images confirmed spent - signedOfferKeyImagePoller.addListener(new XmrKeyImageListener() { - @Override - public void onSpentStatusChanged(Map spentStatuses) { - for (Entry entry : spentStatuses.entrySet()) { - if (entry.getValue() == MoneroKeyImageSpentStatus.CONFIRMED) { - removeSignedOffers(entry.getKey()); - } - } - } - }); - - // first poll in 5s - // TODO: remove? - new Thread(() -> { - HavenoUtils.waitFor(5000); - signedOfferKeyImagePoller.poll(); - }).start(); - } - - private long getKeyImageRefreshPeriodMs() { - return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE; - } - public void onAllServicesInitialized() { p2PService.addDecryptedDirectMessageListener(this); @@ -312,7 +263,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void cleanUpAddressEntries() { - Set openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + Set openOffersIdSet; + synchronized (openOffers.getList()) { + openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + } xmrWalletService.getAddressEntriesForOpenOffer().stream() .filter(e -> !openOffersIdSet.contains(e.getOfferId())) .forEach(e -> { @@ -328,7 +282,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopped = true; p2PService.getPeerManager().removeListener(this); p2PService.removeDecryptedDirectMessageListener(this); - if (signedOfferKeyImagePoller != null) signedOfferKeyImagePoller.clearKeyImages(); + xmrConnectionService.getKeyImagePoller().removeKeyImages(OPEN_OFFER_GROUP_KEY_IMAGE_ID); + xmrConnectionService.getKeyImagePoller().removeKeyImages(SIGNED_OFFER_KEY_IMAGE_GROUP_ID); stopPeriodicRefreshOffersTimer(); stopPeriodicRepublishOffersTimer(); @@ -342,7 +297,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ThreadUtils.execute(() -> { // remove offers from offer book - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.forEach(openOffer -> { if (openOffer.getState() == OpenOffer.State.AVAILABLE) { offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload()); @@ -383,20 +338,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffers(getObservableList(), completeHandler); } - public void removeOpenOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { - removeOpenOffers(List.of(openOffer), completeHandler); - } - - public void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { - int size = openOffers.size(); - // Copy list as we remove in the loop - List openOffersList = new ArrayList<>(openOffers); - openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> { - }, errorMessage -> { - log.warn("Error removing open offer: " + errorMessage); - })); - if (completeHandler != null) - UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); + private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { + synchronized (openOffers) { + int size = openOffers.size(); + // Copy list as we remove in the loop + List openOffersList = new ArrayList<>(openOffers); + openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> { + }, errorMessage -> { + log.warn("Error removing open offer: " + errorMessage); + })); + if (completeHandler != null) + UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS); + } } @@ -440,6 +393,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe maybeUpdatePersistedOffers(); + // listen for spent key images to close open and signed offers + xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() { + @Override + public void onSpentStatusChanged(Map spentStatuses) { + for (Entry entry : spentStatuses.entrySet()) { + if (XmrKeyImagePoller.isSpent(entry.getValue())) { + cancelOpenOffersOnSpent(entry.getKey()); + removeSignedOffers(entry.getKey()); + } + } + } + }); + // run off user thread so app is not blocked from starting ThreadUtils.submitToPool(() -> { @@ -473,29 +439,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); - // process pending offers - processPendingOffers(false, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on bootstrap: " + errorMessage); + // processs offers + processOffers(false, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on bootstrap: " + errorMessage); }); - // register to process pending offers on new block + // register to process offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - // process each pending offer on new block a few times, then rely on period republish - processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); + // process each offer on new block a few times, then rely on period republish + processOffers(true, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on new block {}: {}", height, errorMessage); }); } }); - // initialize key image poller for signed offers - maybeInitializeKeyImagePoller(); + // poll spent status of open offer key images + synchronized (openOffers.getList()) { + for (OpenOffer openOffer : openOffers.getList()) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } + } - // poll spent status of key images - for (SignedOffer signedOffer : signedOffers.getList()) { - signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + // poll spent status of signed offer key images + synchronized (signedOffers.getList()) { + for (SignedOffer signedOffer : signedOffers.getList()) { + xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); + } } }, THREAD_ID); }); @@ -542,24 +514,66 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe long triggerPrice, boolean reserveExactAmount, boolean resetAddressEntriesOnError, + String sourceOfferId, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - - // create open offer - OpenOffer openOffer = new OpenOffer(offer, triggerPrice, reserveExactAmount); - - // schedule or post offer ThreadUtils.execute(() -> { + + // check source offer and clone limit + OpenOffer sourceOffer = null; + if (sourceOfferId != null) { + + // get source offer + Optional sourceOfferOptional = getOpenOffer(sourceOfferId); + if (!sourceOfferOptional.isPresent()) { + 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."); + return; + } + } + + // create open offer + OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount()); + + // set state from source offer + if (sourceOffer != null) { + openOffer.setReserveTxHash(sourceOffer.getReserveTxHash()); + openOffer.setReserveTxHex(sourceOffer.getReserveTxHex()); + openOffer.setReserveTxKey(sourceOffer.getReserveTxKey()); + openOffer.setGroupId(sourceOffer.getGroupId()); + openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId()); + if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED); + } + + // add the open offer + synchronized (processOffersLock) { + addOpenOffer(openOffer); + } + + // done if source offer is pending + if (sourceOffer != null && sourceOffer.isPending()) { + resultHandler.handleResult(null); + return; + } + + // schedule or post offer synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); - addOpenOffer(openOffer); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { + processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { if (!openOffer.isCanceled()) { - log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); + log.warn("Error processing offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer, resetAddressEntriesOnError); } latch.countDown(); @@ -572,12 +586,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Remove from offerbook public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(offer.getId()); + Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent()) { cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); } else { - log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook."); - errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook."); + String errorMsg = "Offer was not found in our list of open offers. We still try to remove it from the offerbook."; + log.warn(errorMsg); + errorMessageHandler.handleErrorMessage(errorMsg); offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null); } } @@ -588,28 +603,49 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isPending()) { resultHandler.handleResult(); // ignore if pending } else if (offersToBeEdited.containsKey(openOffer.getId())) { - errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning")); + } else if (hasConflictingClone(openOffer)) { + errorMessageHandler.handleErrorMessage(Res.get("offerbook.hasConflictingClone.warning")); } else { - Offer offer = openOffer.getOffer(); - offerBookService.activateOffer(offer, - () -> { - openOffer.setState(OpenOffer.State.AVAILABLE); - requestPersistence(); - log.debug("activateOpenOffer, offerId={}", offer.getId()); - resultHandler.handleResult(); - }, - errorMessageHandler); + try { + + // validate arbitrator signature + validateSignedState(openOffer); + + // activate offer on offer book + Offer offer = openOffer.getOffer(); + offerBookService.activateOffer(offer, + () -> { + openOffer.setState(OpenOffer.State.AVAILABLE); + applyTriggerState(openOffer); + requestPersistence(); + log.debug("activateOpenOffer, offerId={}", offer.getId()); + resultHandler.handleResult(); + }, + errorMessageHandler); + } catch (Exception e) { + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } + } + } + + private void applyTriggerState(OpenOffer openOffer) { + if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; + if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) { + openOffer.deactivate(true); } } public void deactivateOpenOffer(OpenOffer openOffer, + boolean deactivatedByTrigger, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = openOffer.getOffer(); if (openOffer.isAvailable()) { offerBookService.deactivateOffer(offer.getOfferPayload(), () -> { - openOffer.setState(OpenOffer.State.DEACTIVATED); + openOffer.deactivate(deactivatedByTrigger); requestPersistence(); log.debug("deactivateOpenOffer, offerId={}", offer.getId()); resultHandler.handleResult(); @@ -643,7 +679,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); } } else { - if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't remove an offer that is currently edited."); + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't cancel an offer that is currently edited."); } } @@ -660,6 +696,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isAvailable()) { deactivateOpenOffer(openOffer, + false, resultHandler, errorMessage -> { offersToBeEdited.remove(openOffer.getId()); @@ -675,7 +712,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(editedOffer.getId()); + Optional openOfferOptional = getOpenOffer(editedOffer.getId()); if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); @@ -685,16 +722,45 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffer(openOffer); OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer); - editedOpenOffer.setState(originalState); + if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) { + if (hasConflictingClone(editedOpenOffer)) { + editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); + } else { + editedOpenOffer.setState(OpenOffer.State.AVAILABLE); + } + applyTriggerState(editedOpenOffer); + } else { + if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) { + editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); + } else { + editedOpenOffer.setState(originalState); + } + } addOpenOffer(editedOpenOffer); - if (editedOpenOffer.isAvailable()) - maybeRepublishOffer(editedOpenOffer); + // check for valid arbitrator signature after editing + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) { - offersToBeEdited.remove(openOffer.getId()); - requestPersistence(); - resultHandler.handleResult(); + // reset arbitrator signature + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + + // process offer to sign and publish + processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + }, (errorMsg) -> { + errorMessageHandler.handleErrorMessage(errorMsg); + }); + } else { + maybeRepublishOffer(editedOpenOffer, null); + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + } } else { errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); } @@ -711,6 +777,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { resultHandler.handleResult(); } + requestPersistence(); } else { errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); } @@ -720,31 +787,38 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe doCancelOffer(openOffer, true); } - // remove open offer which thaws its key images + // cancel open offer which thaws its key images private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) { Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); + boolean hasClonedOffer = hasClonedOffer(offer.getId()); // record before removing open offer removeOpenOffer(openOffer); - closedTradableManager.add(openOffer); // TODO: don't add these to closed tradables? + if (!hasClonedOffer) closedTradableManager.add(openOffer); // do not add clones to closed trades TODO: don't add canceled offers to closed tradables? if (resetAddressEntries) xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); requestPersistence(); - xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); + if (!hasClonedOffer) xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); } - // close open offer after key images spent - public void closeOpenOffer(Offer offer) { - getOpenOfferById(offer.getId()).ifPresent(openOffer -> { - removeOpenOffer(openOffer); - openOffer.setState(OpenOffer.State.CLOSED); - xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); - offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), - () -> log.info("Successfully removed offer {}", offer.getId()), - log::error); - requestPersistence(); + // close open offer group after key images spent + public void closeSpentOffer(Offer offer) { + getOpenOffer(offer.getId()).ifPresent(openOffer -> { + for (OpenOffer groupOffer: getOpenOfferGroup(openOffer.getGroupId())) { + doCloseOpenOffer(groupOffer); + } }); } + private void doCloseOpenOffer(OpenOffer openOffer) { + removeOpenOffer(openOffer); + openOffer.setState(OpenOffer.State.CLOSED); + xmrWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); + offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), + () -> log.info("Successfully removed offer {}", openOffer.getId()), + log::error); + requestPersistence(); + } + public void reserveOpenOffer(OpenOffer openOffer) { openOffer.setState(OpenOffer.State.RESERVED); requestPersistence(); @@ -755,6 +829,37 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe requestPersistence(); } + public boolean hasConflictingClone(OpenOffer openOffer) { + for (OpenOffer clonedOffer : getOpenOfferGroup(openOffer.getGroupId())) { + if (clonedOffer.getId().equals(openOffer.getId())) continue; + if (clonedOffer.isDeactivated()) continue; // deactivated offers do not conflict + + // pending offers later in the order do not conflict + List openOffers = getOpenOffers(); + if (clonedOffer.isPending() && openOffers.indexOf(clonedOffer) > openOffers.indexOf(openOffer)) { + continue; + } + + // conflicts if same payment method and currency + if (samePaymentMethodAndCurrency(clonedOffer.getOffer(), openOffer.getOffer())) { + return true; + } + } + return false; + } + + public boolean hasConflictingClone(Offer offer, OpenOffer sourceOffer) { + return getOpenOfferGroup(sourceOffer.getGroupId()).stream() + .filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers + .anyMatch(openOffer -> samePaymentMethodAndCurrency(openOffer.getOffer(), offer)); + } + + private boolean samePaymentMethodAndCurrency(Offer offer1, Offer offer2) { + return offer1.getPaymentMethodId().equalsIgnoreCase(offer2.getPaymentMethodId()) && + offer1.getCounterCurrencyCode().equalsIgnoreCase(offer2.getCounterCurrencyCode()) && + offer1.getBaseCurrencyCode().equalsIgnoreCase(offer2.getBaseCurrencyCode()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -763,32 +868,51 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return offer.isMyOffer(keyRing); } - public boolean hasOpenOffers() { - synchronized (openOffers) { - for (OpenOffer openOffer : getOpenOffers()) { - if (openOffer.getState() == OpenOffer.State.AVAILABLE) { - return true; - } + public boolean hasAvailableOpenOffers() { + for (OpenOffer openOffer : getOpenOffers()) { + if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + return true; } - return false; } + return false; } public List getOpenOffers() { - synchronized (openOffers) { - return new ArrayList<>(getObservableList()); + synchronized (openOffers.getList()) { + return ImmutableList.copyOf(getObservableList()); } } + public List getOpenOfferGroup(String groupId) { + if (groupId == null) throw new IllegalArgumentException("groupId cannot be null"); + return getOpenOffers().stream() + .filter(openOffer -> groupId.equals(openOffer.getGroupId())) + .collect(Collectors.toList()); + } + + public boolean hasClonedOffer(String offerId) { + OpenOffer openOffer = getOpenOffer(offerId).orElse(null); + if (openOffer == null) return false; + return getOpenOfferGroup(openOffer.getGroupId()).size() > 1; + } + + public boolean hasClonedOffers() { + for (OpenOffer openOffer : getOpenOffers()) { + if (getOpenOfferGroup(openOffer.getGroupId()).size() > 1) { + return true; + } + } + return false; + } + public List getSignedOffers() { - synchronized (signedOffers) { - return new ArrayList<>(signedOffers.getObservableList()); + synchronized (signedOffers.getList()) { + return ImmutableList.copyOf(signedOffers.getObservableList()); } } - public ObservableList getObservableSignedOffersList() { - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { return signedOffers.getObservableList(); } } @@ -797,43 +921,62 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return openOffers.getObservableList(); } - public Optional getOpenOfferById(String offerId) { - synchronized (openOffers) { - return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); - } + public Optional getOpenOffer(String offerId) { + return getOpenOffers().stream().filter(e -> e.getId().equals(offerId)).findFirst(); } public boolean hasOpenOffer(String offerId) { - return getOpenOfferById(offerId).isPresent(); + return getOpenOffer(offerId).isPresent(); } public Optional getSignedOfferById(String offerId) { - synchronized (signedOffers) { - return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); - } + return getSignedOffers().stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); } private void addOpenOffer(OpenOffer openOffer) { log.info("Adding open offer {}", openOffer.getId()); - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.add(openOffer); + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } } private void removeOpenOffer(OpenOffer openOffer) { log.info("Removing open offer {}", openOffer.getId()); - synchronized (openOffers) { + synchronized (openOffers.getList()) { openOffers.remove(openOffer); + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) { + xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID); + } } - synchronized (placeOfferProtocols) { - PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); - if (protocol != null) protocol.cancelOffer(); + + // cancel place offer protocol + ThreadUtils.execute(() -> { + synchronized (processOffersLock) { + synchronized (placeOfferProtocols) { + PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId()); + if (protocol != null) protocol.cancelOffer(); + } + } + }, THREAD_ID); + } + + private void cancelOpenOffersOnSpent(String keyImage) { + synchronized (openOffers.getList()) { + for (OpenOffer openOffer : openOffers.getList()) { + if (openOffer.getState() != OpenOffer.State.RESERVED && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { + log.warn("Canceling open offer because reserved funds have been spent unexpectedly, offerId={}, state={}", openOffer.getId(), openOffer.getState()); + cancelOpenOffer(openOffer, null, null); + } + } } } private void addSignedOffer(SignedOffer signedOffer) { log.info("Adding SignedOffer for offer {}", signedOffer.getOfferId()); - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { // remove signed offers with common key images for (String keyImage : signedOffer.getReserveTxKeyImages()) { @@ -842,22 +985,24 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // add new signed offer signedOffers.add(signedOffer); - signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages()); + xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } } private void removeSignedOffer(SignedOffer signedOffer) { log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId()); - synchronized (signedOffers) { + synchronized (signedOffers.getList()) { signedOffers.remove(signedOffer); - signedOfferKeyImagePoller.removeKeyImages(signedOffer.getReserveTxKeyImages()); } + xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID); } private void removeSignedOffers(String keyImage) { - for (SignedOffer signedOffer : new ArrayList(signedOffers.getList())) { - if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { - removeSignedOffer(signedOffer); + synchronized (signedOffers.getList()) { + for (SignedOffer signedOffer : getSignedOffers()) { + if (signedOffer.getReserveTxKeyImages().contains(keyImage)) { + removeSignedOffer(signedOffer); + } } } } @@ -865,30 +1010,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - private void processPendingOffers(boolean skipOffersWithTooManyAttempts, + private void processOffers(boolean skipOffersWithTooManyAttempts, TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); - for (OpenOffer pendingOffer : openOffers) { - if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; - if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts + for (OpenOffer offer : openOffers) { + if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(openOffers, pendingOffer, (transaction) -> { + processOffer(openOffers, offer, (transaction) -> { latch.countDown(); }, errorMessage -> { - if (!pendingOffer.isCanceled()) { - String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage; - errorMessages.add(warnMessage); - - // cancel offer if invalid - if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId()); - doCancelOffer(pendingOffer); - } - } + errorMessages.add(errorMessage); latch.countDown(); }); HavenoUtils.awaitLatch(latch); @@ -903,7 +1038,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } - private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { @@ -913,32 +1048,87 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // process offer openOffer.setProcessing(true); - doProcessPendingOffer(openOffers, openOffer, (transaction) -> { + doProcessOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.getOffer().setErrorMessage(errorMsg); + if (!openOffer.isCanceled()) { + errorMsg = "Error processing offer, offerId=" + openOffer.getId() + ", attempt=" + openOffer.getNumProcessingAttempts() + ": " + errorMsg; + openOffer.getOffer().setErrorMessage(errorMsg); + + // cancel offer if invalid + if (openOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); + doCancelOffer(openOffer); + } + } errorMessageHandler.handleErrorMessage(errorMsg); }); } - private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void doProcessOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { - // done processing if wallet not initialized - if (xmrWalletService.getWallet() == null) { + // done processing if canceled or wallet not initialized + if (openOffer.isCanceled() || xmrWalletService.getWallet() == null) { resultHandler.handleResult(null); return; } + // validate offer + try { + ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user); + } catch (Exception e) { + errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage()); + return; + } + + // handle pending offer + if (openOffer.isPending()) { + + // only process the first offer of a pending clone group + if (openOffer.getGroupId() != null) { + List openOfferClones = getOpenOfferGroup(openOffer.getGroupId()); + if (openOfferClones.size() > 1 && !openOfferClones.get(0).getId().equals(openOffer.getId()) && openOfferClones.get(0).isPending()) { + resultHandler.handleResult(null); + return; + } + } + } else { + + // validate non-pending state + boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first + if (!skipValidation) { + try { + 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); + } + } + } + + // sign and post offer if already funded + if (openOffer.getReserveTxHash() != null) { + signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); + return; + } + // cancel offer if scheduled txs unavailable if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; for (MoneroTxWallet tx : xmrWalletService.getTxs(openOffer.getScheduledTxHashes())) { - if (!tx.isLocked() && !isOutputsAvailable(tx)) { + if (!tx.isLocked() && !hasSpendableAmount(tx)) { scheduledTxsAvailable = false; break; } @@ -963,48 +1153,50 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe setSplitOutputTx(openOffer, splitOutputTx); } - // if not found, create tx to split exact output - if (splitOutputTx == null) { - if (openOffer.getSplitOutputTxHash() != null) { - log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); - setSplitOutputTx(openOffer, null); - } - try { - splitOrSchedule(openOffers, openOffer, amountNeeded); - } catch (Exception e) { - log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); - openOffer.getOffer().setState(Offer.State.INVALID); - errorMessageHandler.handleErrorMessage(e.getMessage()); - return; - } - } else if (!splitOutputTx.isLocked()) { - - // otherwise sign and post offer if split output available - signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + // if wallet has exact available balance, try to sign and post directly + if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) { + signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); + }); return; + } else { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); } } else { // sign and post offer if enough funds - boolean hasFundsReserved = openOffer.getReserveTxHash() != null; boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0; - if (hasFundsReserved || hasSufficientBalance) { + if (hasSufficientBalance) { signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); + resultHandler.handleResult(null); + return; } } - - // handle result - resultHandler.handleResult(null); } catch (Exception e) { - if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e); + if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); } }).start(); } + private void validateSignedState(OpenOffer openOffer) { + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signer"); + } 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"); + } 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()) { + throw new IllegalArgumentException("Offer " + openOffer.getId() + " is missing reserve tx hash or key images"); + } + } + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); @@ -1063,13 +1255,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); } } - if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); + if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); } splitOutputTxs.removeAll(removeTxs); return splitOutputTxs; } - private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { + private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) @@ -1091,7 +1283,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return earliestUnscheduledTx; } - private void splitOrSchedule(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { + // if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output + private void splitOrSchedule(MoneroTxWallet splitOutputTx, List openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (splitOutputTx == null) { + if (openOffer.getSplitOutputTxHash() != null) { + log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); + setSplitOutputTx(openOffer, null); + } + try { + splitOrScheduleAux(openOffers, openOffer, amountNeeded); + resultHandler.handleResult(null); + return; + } catch (Exception e) { + log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); + openOffer.getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } + } else if (!splitOutputTx.isLocked()) { + + // otherwise sign and post offer if split output available + signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + return; + } else { + resultHandler.handleResult(null); + return; + } + } + + private void splitOrScheduleAux(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0; @@ -1149,34 +1369,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { - // check for sufficient balance - scheduled offers amount + // get earliest available or pending txs with sufficient spendable amount BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded(); - if (xmrWalletService.getBalance().subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) { - throw new RuntimeException("Not enough money in Haveno wallet"); - } - - // get earliest available or pending txs with sufficient incoming amount BigInteger scheduledAmount = BigInteger.ZERO; Set scheduledTxs = new HashSet(); for (MoneroTxWallet tx : xmrWalletService.getTxs()) { - // skip if outputs unavailable - if (tx.getIncomingTransfers() == null || tx.getIncomingTransfers().isEmpty()) continue; - if (!isOutputsAvailable(tx)) continue; - if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue; + // get unscheduled spendable amount + BigInteger spendableAmount = getUnscheduledSpendableAmount(tx, openOffers); - // add scheduled tx - for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { - if (transfer.getAccountIndex() == 0) { - scheduledAmount = scheduledAmount.add(transfer.getAmount()); - scheduledTxs.add(tx); - } - } + // skip if no spendable amount + if (spendableAmount.equals(BigInteger.ZERO)) continue; + + // schedule tx + scheduledAmount = scheduledAmount.add(spendableAmount); + scheduledTxs.add(tx); // break if sufficient funds if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break; } - if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer"); + if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to create offer"); // schedule txs openOffer.setScheduledTxHashes(scheduledTxs.stream().map(tx -> tx.getHash()).collect(Collectors.toList())); @@ -1184,21 +1396,56 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setState(OpenOffer.State.PENDING); } - private BigInteger getScheduledAmount(List openOffers) { - BigInteger scheduledAmount = BigInteger.ZERO; + private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List openOffers) { + if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO; + return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO); + } + + private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List openOffers) { for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.PENDING) continue; if (openOffer.getScheduledTxHashes() == null) continue; - List fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); - for (MoneroTxWallet fundingTx : fundingTxs) { - if (fundingTx.getIncomingTransfers() != null) { - for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) { - if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount()); + if (openOffer.getScheduledTxHashes().contains(tx.getHash()) && !tx.getHash().equals(openOffer.getSplitOutputTxHash())) { + return true; + } + } + return false; + } + + private BigInteger getSplitAmount(MoneroTxWallet tx, List openOffers) { + for (OpenOffer openOffer : openOffers) { + if (openOffer.getSplitOutputTxHash() == null) continue; + if (!openOffer.getSplitOutputTxHash().equals(tx.getHash())) continue; + return openOffer.getOffer().getAmountNeeded(); + } + return BigInteger.ZERO; + } + + private BigInteger getSpendableAmount(MoneroTxWallet tx) { + + // compute spendable amount from outputs if confirmed + if (tx.isConfirmed()) { + BigInteger spendableAmount = BigInteger.ZERO; + if (tx.getOutputsWallet() != null) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (!output.isSpent() && !output.isFrozen() && output.getAccountIndex() == 0) { + spendableAmount = spendableAmount.add(output.getAmount()); } } } + return spendableAmount; } - return scheduledAmount; + + // funds sent to self always show 0 incoming amount, so compute from destinations manually + // TODO: this excludes change output, so change is missing from spendable amount until confirmed + BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); + if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) return sentToSelfAmount; + + // if not confirmed and not sent to self, return incoming amount + return tx.getIncomingAmount() == null ? BigInteger.ZERO : tx.getIncomingAmount(); + } + + private boolean hasSpendableAmount(MoneroTxWallet tx) { + return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0; } private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { @@ -1215,17 +1462,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return false; } - private boolean isOutputsAvailable(MoneroTxWallet tx) { - if (tx.getOutputsWallet() == null) return false; - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (output.isSpent() || output.isFrozen()) return false; - } - return true; - } - private void signAndPostOffer(OpenOffer openOffer, boolean useSavingsWallet, // TODO: remove this? - TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + TransactionResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { log.info("Signing and posting offer " + openOffer.getId()); // create model @@ -1251,18 +1491,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe transaction -> { // set offer state - openOffer.setState(OpenOffer.State.AVAILABLE); openOffer.setScheduledTxHashes(null); openOffer.setScheduledAmount(null); requestPersistence(); - resultHandler.handleResult(transaction); if (!stopped) { startPeriodicRepublishOffersTimer(); startPeriodicRefreshOffersTimer(); } else { log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } + resultHandler.handleResult(transaction); }, errorMessageHandler); @@ -1290,7 +1529,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { errorMessage = "Cannot sign offer because we are not a registered arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } @@ -1298,47 +1537,135 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // verify arbitrator is signer of offer payload if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) { errorMessage = "Cannot sign offer because offer payload is for a different arbitrator"; - log.info(errorMessage); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - // verify maker's trade fee + // private offers must have challenge hash Offer offer = new Offer(request.getOfferPayload()); - if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) { - errorMessage = "Wrong maker fee for offer " + request.offerId; - log.info(errorMessage); + if (offer.isPrivateOffer() && (offer.getChallengeHash() == null || offer.getChallengeHash().length() == 0)) { + errorMessage = "Private offer must have challenge hash for offer " + request.offerId; + 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; - log.info(errorMessage); + // 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(); + log.warn(errorMessage); sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } + // verify the trade protocol version + if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) { + errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify the min version number + if (filterManager.getDisableTradeBelowVersion() != null) { + if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) { + errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion(); + 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(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's trade fee + if (offer.getTakerFeePct() != 0) { + errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // 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(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify taker's security deposit + if (offer.getBuyerSecurityDepositPct() != 0) { + errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } else { + + // 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(); + 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(); + 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(); + 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(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // security deposits must be equal + if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { + errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } + // verify penalty fee if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) { errorMessage = "Wrong penalty fee for offer " + request.offerId; - log.info(errorMessage); - sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); - return; - } - - // verify security deposits are equal - if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) { - errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId; - log.info(errorMessage); + 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(), HavenoUtils.MAKER_FEE_PCT); + BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); MoneroTx verifiedTx = xmrWalletService.verifyReserveTx( @@ -1452,6 +1779,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // Don't allow trade start if not connected to Monero node + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) { + errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node."; + log.info(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + if (stopped) { errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; log.debug(errorMessage); @@ -1470,7 +1805,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } try { - Optional openOfferOptional = getOpenOfferById(request.offerId); + Optional openOfferOptional = getOpenOffer(request.offerId); AvailabilityResult availabilityResult; byte[] makerSignature = null; if (openOfferOptional.isPresent()) { @@ -1612,7 +1947,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// private void maybeUpdatePersistedOffers() { - // We need to clone to avoid ConcurrentModificationException List openOffersClone = getOpenOffers(); openOffersClone.forEach(originalOpenOffer -> { Offer originalOffer = originalOpenOffer.getOffer(); @@ -1684,7 +2018,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getBankId(), originalOfferPayload.getAcceptedBankIds(), - originalOfferPayload.getVersionNr(), + Version.VERSION, originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradePeriod(), @@ -1693,29 +2027,23 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getLowerClosePrice(), originalOfferPayload.getUpperClosePrice(), originalOfferPayload.isPrivateOffer(), - originalOfferPayload.getHashOfChallenge(), + originalOfferPayload.getChallengeHash(), updatedExtraDataMap, protocolVersion, - originalOfferPayload.getArbitratorSigner(), - originalOfferPayload.getArbitratorSignature(), - originalOfferPayload.getReserveTxKeyImages()); + null, + null, + null, + originalOfferPayload.getExtraInfo()); - // Save states from original data to use for the updated - Offer.State originalOfferState = originalOffer.getState(); - OpenOffer.State originalOpenOfferState = originalOpenOffer.getState(); + // cancel old offer + log.info("Canceling outdated offer id={}", originalOffer.getId()); + doCancelOffer(originalOpenOffer, false); - // remove old offer - originalOffer.setState(Offer.State.REMOVED); - originalOpenOffer.setState(OpenOffer.State.CANCELED); - removeOpenOffer(originalOpenOffer); - - // Create new Offer + // create new offer Offer updatedOffer = new Offer(updatedPayload); updatedOffer.setPriceFeedService(priceFeedService); - updatedOffer.setState(originalOfferState); OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); - updatedOpenOffer.setState(originalOpenOfferState); addOpenOffer(updatedOpenOffer); requestPersistence(); @@ -1737,10 +2065,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopPeriodicRefreshOffersTimer(); ThreadUtils.execute(() -> { - processListForRepublishOffers(getOpenOffers()); + processListForRepublishOffers(new ArrayList<>(getOpenOffers())); // list will be modified }, THREAD_ID); } + // modifies the given list private void processListForRepublishOffers(List list) { if (list.isEmpty()) { return; @@ -1748,7 +2077,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer openOffer = list.remove(0); boolean contained = false; - synchronized (openOffers) { + synchronized (openOffers.getList()) { contained = openOffers.contains(openOffer); } if (contained) { @@ -1767,10 +2096,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void maybeRepublishOffer(OpenOffer openOffer) { - maybeRepublishOffer(openOffer, null); - } - private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { ThreadUtils.execute(() -> { @@ -1780,82 +2105,58 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - // determine if offer is valid - boolean isValid = true; - Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (arbitrator == null) { - log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); - isValid = false; - } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { - log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); - isValid = false; - } - if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { - log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); - isValid = false; - } + // reprocess offer then publish + synchronized (processOffersLock) { + CountDownLatch latch = new CountDownLatch(1); + processOffer(getOpenOffers(), openOffer, (transaction) -> { + requestPersistence(); + latch.countDown(); - // if valid, re-add offer to book - if (isValid) { - offerBookService.addOffer(openOffer.getOffer(), - () -> { - if (!stopped) { - - // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) - if (periodicRefreshOffersTimer == null) { - startPeriodicRefreshOffersTimer(); - } - if (completeHandler != null) { - completeHandler.run(); - } - } - }, - errorMessage -> { - if (!stopped) { - log.error("Adding offer to P2P network failed. " + errorMessage); - stopRetryRepublishOffersTimer(); - retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, - RETRY_REPUBLISH_DELAY_SEC); - if (completeHandler != null) completeHandler.run(); - } - }); - } else { - - // reset offer state to pending - openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); - openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - openOffer.getOffer().setState(Offer.State.UNKNOWN); - openOffer.setState(OpenOffer.State.PENDING); - - // republish offer - synchronized (processOffersLock) { - CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { - requestPersistence(); - latch.countDown(); + // skip if prevented from publishing + if (preventedFromPublishing(openOffer)) { if (completeHandler != null) completeHandler.run(); - }, (errorMessage) -> { - if (!openOffer.isCanceled()) { - log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); - openOffer.getOffer().setErrorMessage(errorMessage); + return; + } + + // publish offer to books + offerBookService.addOffer(openOffer.getOffer(), + () -> { + if (!stopped) { - // cancel offer if invalid - if (openOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); - doCancelOffer(openOffer); - } - } - latch.countDown(); - if (completeHandler != null) completeHandler.run(); - }); - HavenoUtils.awaitLatch(latch); - } + // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) + if (periodicRefreshOffersTimer == null) { + startPeriodicRefreshOffersTimer(); + } + if (completeHandler != null) { + completeHandler.run(); + } + } + }, + errorMessage -> { + if (!stopped) { + log.error("Adding offer to P2P network failed. " + errorMessage); + stopRetryRepublishOffersTimer(); + retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, + RETRY_REPUBLISH_DELAY_SEC); + if (completeHandler != null) completeHandler.run(); + } + }); + }, (errorMessage) -> { + log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); + latch.countDown(); + if (completeHandler != null) completeHandler.run(); + }); + HavenoUtils.awaitLatch(latch); } }, THREAD_ID); } private boolean preventedFromPublishing(OpenOffer openOffer) { - return openOffer.isDeactivated() || openOffer.isCanceled(); + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; + return openOffer.isDeactivated() || + openOffer.isCanceled() || + openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null || + hasConflictingClone(openOffer); } private void startPeriodicRepublishOffersTimer() { @@ -1877,25 +2178,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { - int size = openOffers.size(); - //we clone our list as openOffers might change during our delayed call - final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); - for (int i = 0; i < size; i++) { - // we delay to avoid reaching throttle limits - // roughly 4 offers per second - - long delay = 300; - final long minDelay = (i + 1) * delay; - final long maxDelay = (i + 2) * delay; - final OpenOffer openOffer = openOffersList.get(i); - UserThread.runAfterRandomDelay(() -> { - // we need to check if in the meantime the offer has been removed - boolean contained = false; - synchronized (openOffers) { - contained = openOffers.contains(openOffer); - } - if (contained) maybeRefreshOffer(openOffer, 0, 1); - }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + synchronized (openOffers.getList()) { + int size = openOffers.size(); + //we clone our list as openOffers might change during our delayed call + final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); + for (int i = 0; i < size; i++) { + // we delay to avoid reaching throttle limits + // roughly 4 offers per second + + long delay = 300; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + final OpenOffer openOffer = openOffersList.get(i); + UserThread.runAfterRandomDelay(() -> { + // we need to check if in the meantime the offer has been removed + boolean contained = false; + synchronized (openOffers.getList()) { + contained = openOffers.contains(openOffer); + } + if (contained) maybeRefreshOffer(openOffer, 0, 1); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + } } } else { log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); diff --git a/core/src/main/java/haveno/core/offer/SignedOfferList.java b/core/src/main/java/haveno/core/offer/SignedOfferList.java index a77d0402d9..4d75a14dd4 100644 --- a/core/src/main/java/haveno/core/offer/SignedOfferList.java +++ b/core/src/main/java/haveno/core/offer/SignedOfferList.java @@ -47,10 +47,12 @@ public final class SignedOfferList extends PersistableListAsObservable openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) .forEach(marketPrice -> { openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() - .filter(openOffer -> !openOffer.isDeactivated()) .forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); }); } - public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) { + public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) { Price price = openOffer.getOffer().getPrice(); if (price == null || marketPrice == null) { return false; @@ -125,13 +124,12 @@ public class TriggerPriceService { } private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { - if (wasTriggered(marketPrice, openOffer)) { - String currencyCode = openOffer.getOffer().getCurrencyCode(); - int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? - TraditionalMoney.SMALLEST_UNIT_EXPONENT : - CryptoMoney.SMALLEST_UNIT_EXPONENT; - long triggerPrice = openOffer.getTriggerPrice(); + String currencyCode = openOffer.getOffer().getCurrencyCode(); + int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ? + TraditionalMoney.SMALLEST_UNIT_EXPONENT : + CryptoMoney.SMALLEST_UNIT_EXPONENT; + if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) { log.info("Market price exceeded the trigger price of the open offer.\n" + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + "Market price: {};\nTrigger price: {}", @@ -139,14 +137,26 @@ public class TriggerPriceService { currencyCode, openOffer.getOffer().getDirection(), marketPrice.getPrice(), - MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) + MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) ); - openOfferManager.deactivateOpenOffer(openOffer, () -> { + openOfferManager.deactivateOpenOffer(openOffer, true, () -> { + }, errorMessage -> { + }); + } else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) { + log.info("Market price is back within the trigger price of the open offer.\n" + + "We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + + "Market price: {};\nTrigger price: {}", + openOffer.getOffer().getShortId(), + currencyCode, + openOffer.getOffer().getDirection(), + marketPrice.getPrice(), + MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent) + ); + + openOfferManager.activateOpenOffer(openOffer, () -> { }, errorMessage -> { }); - } else if (openOffer.getState() == OpenOffer.State.AVAILABLE) { - // TODO: check if open offer's reserve tx is failed or double spend seen } } diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index 493ded6759..d6080e61e3 100644 --- a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + null); // challenge is required when offer taken // save trade request to later send to arbitrator model.setTradeRequest(tradeRequest); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 0b22d9e40e..f5def0a433 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.locale.Res; import haveno.core.offer.messages.SignOfferResponse; -import haveno.core.offer.placeoffer.tasks.AddToOfferBook; +import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest; @@ -31,6 +31,8 @@ import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.protocol.TradeProtocol; import haveno.network.p2p.NodeAddress; + +import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,8 +41,8 @@ public class PlaceOfferProtocol { private final PlaceOfferModel model; private Timer timeoutTimer; - private final TransactionResultHandler resultHandler; - private final ErrorMessageHandler errorMessageHandler; + private TransactionResultHandler resultHandler; + private ErrorMessageHandler errorMessageHandler; private TaskRunner taskRunner; @@ -89,7 +91,6 @@ public class PlaceOfferProtocol { handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle } - // TODO (woodser): switch to fluent public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) { log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId()); model.setSignOfferResponse(response); @@ -119,7 +120,7 @@ public class PlaceOfferProtocol { () -> { log.debug("sequence at handleSignOfferResponse completed"); stopTimeoutTimer(); - resultHandler.handleResult(model.getTransaction()); // TODO (woodser): XMR transaction instead + handleResult(model.getTransaction()); // TODO: use XMR transaction instead }, (errorMessage) -> { if (model.isOfferAddedToOfferBook()) { @@ -135,27 +136,33 @@ public class PlaceOfferProtocol { ); taskRunner.addTasks( MakerProcessSignOfferResponse.class, - AddToOfferBook.class + MaybeAddToOfferBook.class ); taskRunner.run(); } - public void startTimeoutTimer() { + public synchronized void startTimeoutTimer() { + if (resultHandler == null) return; stopTimeoutTimer(); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } - private void stopTimeoutTimer() { + private synchronized void stopTimeoutTimer() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } - private void handleError(String errorMessage) { + private synchronized void handleResult(Transaction transaction) { + resultHandler.handleResult(transaction); + resetHandlers(); + } + + private synchronized void handleError(String errorMessage) { if (timeoutTimer != null) { taskRunner.cancel(); if (!model.getOpenOffer().isCanceled()) { @@ -164,5 +171,11 @@ public class PlaceOfferProtocol { stopTimeoutTimer(); errorMessageHandler.handleErrorMessage(errorMessage); } + resetHandlers(); + } + + private synchronized void resetHandlers() { + resultHandler = null; + errorMessageHandler = null; } } 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 4b44a8102d..60eaa64cdc 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 @@ -19,7 +19,9 @@ package haveno.core.offer.placeoffer.tasks; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; @@ -33,6 +35,7 @@ import haveno.core.xmr.model.XmrAddressEntry; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroOutput; +import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTxWallet; @Slf4j @@ -62,7 +65,6 @@ public class MakerReserveOfferFunds extends Task { model.getXmrWalletService().getXmrConnectionService().verifyConnection(); // create reserve tx - MoneroTxWallet reserveTx = null; synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { // reset protocol timeout @@ -78,7 +80,14 @@ public class MakerReserveOfferFunds extends Task { XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); + // copy address entries to clones + for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + if (offerClone.getId().equals(offer.getId())) continue; // skip self + model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId()); + } + // attempt creating reserve tx + MoneroTxWallet reserveTx = null; try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { @@ -86,6 +95,9 @@ public class MakerReserveOfferFunds extends Task { try { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); model.getXmrWalletService().handleWalletError(e, sourceConnection); @@ -116,11 +128,43 @@ public class MakerReserveOfferFunds extends Task { List reservedKeyImages = new ArrayList(); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - // update offer state - openOffer.setReserveTxHash(reserveTx.getHash()); - openOffer.setReserveTxHex(reserveTx.getFullHex()); - openOffer.setReserveTxKey(reserveTx.getKey()); - offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + // update offer state including clones + if (openOffer.getGroupId() == null) { + openOffer.setReserveTxHash(reserveTx.getHash()); + openOffer.setReserveTxHex(reserveTx.getFullHex()); + openOffer.setReserveTxKey(reserveTx.getKey()); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + } else { + for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + offerClone.setReserveTxHash(reserveTx.getHash()); + offerClone.setReserveTxHex(reserveTx.getFullHex()); + offerClone.setReserveTxKey(reserveTx.getKey()); + offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + } + } + + // reset offer funding address entries if unused + if (fundingEntry != null) { + + // get reserve tx inputs + List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages); + + // collect subaddress indices of inputs + Set inputSubaddressIndices = new HashSet<>(); + for (MoneroOutputWallet input : inputs) { + if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex()); + } + + // swap funding address entries to available if unused + for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) { + XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); + if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { + if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) { + model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); + } + } + } + } } complete(); } catch (Throwable t) { 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 2037b51d09..3644492735 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 @@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task { offer.getOfferPayload().getReserveTxKeyImages(), returnAddress); - // send request to least used arbitrators until success + // send request to random arbitrators until success sendSignOfferRequests(request, () -> { complete(); }, (errorMessage) -> { diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java similarity index 50% rename from core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java rename to core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java index c5c3cf4f46..2f8a10108b 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java @@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; +import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; import static com.google.common.base.Preconditions.checkNotNull; -public class AddToOfferBook extends Task { +public class MaybeAddToOfferBook extends Task { - public AddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { + public MaybeAddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @@ -35,17 +36,32 @@ public class AddToOfferBook extends Task { try { runInterceptHook(); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); - model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), - () -> { - model.setOfferAddedToOfferBook(true); - complete(); - }, - errorMessage -> { - model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + - "Please check your network connection and try again."); - failed(errorMessage); - }); + // deactivate if conflicting offer exists + if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) { + model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED); + model.setOfferAddedToOfferBook(false); + complete(); + return; + } + + // add to offer book and activate if pending or available + if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) { + model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), + () -> { + model.getOpenOffer().setState(OpenOffer.State.AVAILABLE); + model.setOfferAddedToOfferBook(true); + complete(); + }, + errorMessage -> { + model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + + "Please check your network connection and try again."); + failed(errorMessage); + }); + } else { + complete(); + return; + } } catch (Throwable t) { model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" + "Error message:\n" 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 5f683bacf2..3b1a01beeb 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 @@ -19,10 +19,13 @@ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; +import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.trade.HavenoUtils; import haveno.core.trade.messages.TradeMessage; +import haveno.core.user.User; import org.bitcoinj.core.Coin; import java.math.BigInteger; @@ -41,55 +44,7 @@ public class ValidateOffer extends Task { try { runInterceptHook(); - // Coins - checkBINotNullOrZero(offer.getAmount(), "Amount"); - checkBINotNullOrZero(offer.getMinAmount(), "MinAmount"); - //checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model - checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); - if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); - if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); - if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); - if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); - - // We remove those checks to be more flexible with future changes. - /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, - "createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " + - "MakerFee=" + offer.getMakerFee().toFriendlyString());*/ - /*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value, - "buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " + - "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); - checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value, - "buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " + - "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); - checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value, - "sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " + - "sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/ - /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, - "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ - - long maxAmount = model.getAccountAgeWitnessService().getMyTradeLimit(model.getUser().getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection()); - checkArgument(offer.getAmount().longValueExact() <= maxAmount, - "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR"); - checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); - - checkNotNull(offer.getPrice(), "Price is null"); - if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(), - "Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString()); - - checkArgument(offer.getDate().getTime() > 0, - "Date must not be 0. date=" + offer.getDate().toString()); - - checkNotNull(offer.getCurrencyCode(), "Currency is null"); - checkNotNull(offer.getDirection(), "Direction is null"); - checkNotNull(offer.getId(), "Id is null"); - checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null"); - checkNotNull(offer.getMinAmount(), "MinAmount is null"); - checkNotNull(offer.getPrice(), "Price is null"); - checkNotNull(offer.getVersionNr(), "VersionNr is null"); - checkArgument(offer.getMaxTradePeriod() > 0, - "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod()); - // TODO check upper and lower bounds for fiat - // TODO check rest of new parameters + validateOffer(offer, model.getAccountAgeWitnessService(), model.getUser()); complete(); } catch (Exception e) { @@ -100,42 +55,108 @@ public class ValidateOffer extends Task { } } - public static void checkBINotNullOrZero(BigInteger value, String name) { + public static void validateOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService, User user) { + + // Coins + checkBINotNullOrZero(offer.getAmount(), "Amount"); + checkBINotNullOrZero(offer.getMinAmount(), "MinAmount"); + //checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model + checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit"); + if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct()); + if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct()); + offer.isPrivateOffer(); + if (offer.isPrivateOffer()) { + boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY; + if (isBuyerMaker) { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct()); + } else { + if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + } else { + if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct()); + if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct()); + } + + + // We remove those checks to be more flexible with future changes. + /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value, + "createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " + + "MakerFee=" + offer.getMakerFee().toFriendlyString());*/ + /*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value, + "buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " + + "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); + checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value, + "buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " + + "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString()); + checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value, + "sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " + + "sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/ + /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0, + "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/ + + long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit()); + checkArgument(offer.getAmount().longValueExact() <= maxAmount, + "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR"); + checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount"); + + checkNotNull(offer.getPrice(), "Price is null"); + if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(), + "Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString()); + + checkArgument(offer.getDate().getTime() > 0, + "Date must not be 0. date=" + offer.getDate().toString()); + + checkNotNull(offer.getCurrencyCode(), "Currency is null"); + checkNotNull(offer.getDirection(), "Direction is null"); + checkNotNull(offer.getId(), "Id is null"); + checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null"); + checkNotNull(offer.getMinAmount(), "MinAmount is null"); + checkNotNull(offer.getPrice(), "Price is null"); + checkNotNull(offer.getVersionNr(), "VersionNr is null"); + checkArgument(offer.getMaxTradePeriod() > 0, + "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod()); + // TODO check upper and lower bounds for fiat + // TODO check rest of new parameters + } + + private static void checkBINotNullOrZero(BigInteger value, String name) { checkNotNull(value, name + " is null"); checkArgument(value.compareTo(BigInteger.ZERO) > 0, name + " must be positive. " + name + "=" + value); } - public static void checkCoinNotNullOrZero(Coin value, String name) { + private static void checkCoinNotNullOrZero(Coin value, String name) { checkNotNull(value, name + " is null"); checkArgument(value.isPositive(), name + " must be positive. " + name + "=" + value.toFriendlyString()); } - public static String nonEmptyStringOf(String value) { + private static String nonEmptyStringOf(String value) { checkNotNull(value); checkArgument(value.length() > 0); return value; } - public static long nonNegativeLongOf(long value) { + private static long nonNegativeLongOf(long value) { checkArgument(value >= 0); return value; } - public static Coin nonZeroCoinOf(Coin value) { + private static Coin nonZeroCoinOf(Coin value) { checkNotNull(value); checkArgument(!value.isZero()); return value; } - public static Coin positiveCoinOf(Coin value) { + private static Coin positiveCoinOf(Coin value) { checkNotNull(value); checkArgument(value.isPositive()); return value; } - public static void checkTradeId(String tradeId, TradeMessage tradeMessage) { + private static void checkTradeId(String tradeId, TradeMessage tradeMessage) { checkArgument(tradeId.equals(tradeMessage.getOfferId())); } } 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 85d0b99bcf..49c6da12e9 100644 --- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java @@ -148,7 +148,8 @@ public class TakeOfferModel implements Model { private long getMaxTradeLimit() { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), + offer.hasBuyerAsTakerWithoutDeposit()); } @NotNull diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java index b6f93b364d..1bff92b5cd 100644 --- a/core/src/main/java/haveno/core/payment/AliPayAccount.java +++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java @@ -31,7 +31,34 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public final class AliPayAccount extends PaymentAccount { - public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("AED"), + new TraditionalCurrency("AUD"), + new TraditionalCurrency("CAD"), + new TraditionalCurrency("CHF"), + new TraditionalCurrency("CNY"), + new TraditionalCurrency("CZK"), + new TraditionalCurrency("DKK"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP"), + new TraditionalCurrency("HKD"), + new TraditionalCurrency("IDR"), + new TraditionalCurrency("ILS"), + new TraditionalCurrency("JPY"), + new TraditionalCurrency("KRW"), + new TraditionalCurrency("LKR"), + new TraditionalCurrency("MUR"), + new TraditionalCurrency("MYR"), + new TraditionalCurrency("NOK"), + new TraditionalCurrency("NZD"), + new TraditionalCurrency("PHP"), + new TraditionalCurrency("RUB"), + new TraditionalCurrency("SEK"), + new TraditionalCurrency("SGD"), + new TraditionalCurrency("THB"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("ZAR") + ); public AliPayAccount() { super(PaymentMethod.ALI_PAY); diff --git a/core/src/main/java/haveno/core/payment/F2FAccount.java b/core/src/main/java/haveno/core/payment/F2FAccount.java index b75718ec68..a16b4daf6c 100644 --- a/core/src/main/java/haveno/core/payment/F2FAccount.java +++ b/core/src/main/java/haveno/core/payment/F2FAccount.java @@ -93,7 +93,7 @@ public final class F2FAccount extends CountryBasedPaymentAccount { if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE); if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city")); if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact")); - if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt")); + if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount")); return field; } } diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 6a293b3452..14dd88482b 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -36,6 +36,7 @@ package haveno.core.payment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; @@ -139,6 +140,10 @@ public abstract class PaymentAccount implements PersistablePayload { return getSingleTradeCurrency() == null || CurrencyUtil.isFiatCurrency(getSingleTradeCurrency().getCode()); // TODO: check if trade currencies contain fiat } + public boolean isCryptoCurrency() { + return getSingleTradeCurrency() != null && CurrencyUtil.isCryptoCurrency(getSingleTradeCurrency().getCode()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER @@ -337,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload { // ---------------------------- SERIALIZATION ----------------------------- public String toJson() { - Map jsonMap = new HashMap(); - if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class)); + Gson gson = gsonBuilder.create(); + Map jsonMap = new HashMap<>(); + + 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) { + List list = (List) value; + String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(",")); + entry.setValue(joinedString); + } + } + + jsonMap.putAll(payloadMap); + } + jsonMap.put("accountName", getAccountName()); jsonMap.put("accountId", getId()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); - return gsonBuilder.create().toJson(jsonMap); + return gson.toJson(jsonMap); } /** @@ -374,6 +396,7 @@ public abstract class PaymentAccount implements PersistablePayload { @NonNull public abstract List getInputFieldIds(); + @SuppressWarnings("unchecked") public PaymentAccountForm toForm() { // convert to json map diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java b/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java index 64c8bdbc52..d2f6ab2bbc 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountFactory.java @@ -136,6 +136,8 @@ public class PaymentAccountFactory { return new CashAppAccount(); case PaymentMethod.VENMO_ID: return new VenmoAccount(); + case PaymentMethod.PAYSAFE_ID: + return new PaysafeAccount(); // Cannot be deleted as it would break old trade history entries case PaymentMethod.OK_PAY_ID: diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountList.java b/core/src/main/java/haveno/core/payment/PaymentAccountList.java index f9045241f2..8276378747 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountList.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountList.java @@ -36,10 +36,12 @@ public class PaymentAccountList extends PersistableList { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() - .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) - .build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder() + .setPaymentAccountList(protobuf.PaymentAccountList.newBuilder() + .addAllPaymentAccount(getList().stream().map(PaymentAccount::toProtoMessage).collect(Collectors.toList()))) + .build(); + } } public static PaymentAccountList fromProto(protobuf.PaymentAccountList proto, CoreProtoResolver coreProtoResolver) { diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java index 0160a1a736..0226fe4530 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountTypeAdapter.java @@ -24,7 +24,6 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import haveno.core.locale.Country; import haveno.core.locale.CountryUtil; -import haveno.core.locale.TraditionalCurrency; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; import haveno.core.payment.payload.PaymentAccountPayload; @@ -42,7 +41,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import static com.google.common.base.Preconditions.checkNotNull; import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy; import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString; import static haveno.common.util.ReflectionUtils.handleSetFieldValueError; @@ -50,7 +48,6 @@ import static haveno.common.util.ReflectionUtils.isSetterOnClass; import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy; import static haveno.common.util.Utilities.decodeFromHex; import static haveno.core.locale.CountryUtil.findCountryByCode; -import static haveno.core.locale.CurrencyUtil.getCurrencyByCountryCode; import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList; import static haveno.core.locale.CurrencyUtil.getTradeCurrency; import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID; @@ -435,8 +432,10 @@ class PaymentAccountTypeAdapter extends TypeAdapter { if (account.isCountryBasedPaymentAccount()) { ((CountryBasedPaymentAccount) account).setCountry(country.get()); - TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); - account.setSingleTradeCurrency(fiatCurrency); + + // TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere? + // TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode)); + // account.setSingleTradeCurrency(fiatCurrency); } else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) { ((MoneyGramAccount) account).setCountry(country.get()); } else { diff --git a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java index 4d7b4ad0c4..11575aeb62 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccountUtil.java @@ -124,7 +124,7 @@ public class PaymentAccountUtil { AccountAgeWitnessService accountAgeWitnessService) { boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact(); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact(); return !hasChargebackRisk || hasValidAccountAgeWitness; } diff --git a/core/src/main/java/haveno/core/payment/PaysafeAccount.java b/core/src/main/java/haveno/core/payment/PaysafeAccount.java new file mode 100644 index 0000000000..531aa998b6 --- /dev/null +++ b/core/src/main/java/haveno/core/payment/PaysafeAccount.java @@ -0,0 +1,112 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.core.payment; + +import haveno.core.api.model.PaymentAccountFormField; +import haveno.core.locale.TraditionalCurrency; +import haveno.core.locale.TradeCurrency; +import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.payload.PaymentMethod; +import haveno.core.payment.payload.PaysafeAccountPayload; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +public final class PaysafeAccount extends PaymentAccount { + + private static final List INPUT_FIELD_IDS = List.of( + PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EMAIL, + PaymentAccountFormField.FieldId.TRADE_CURRENCIES, + PaymentAccountFormField.FieldId.SALT + ); + + // https://developer.paysafe.com/en/support/reference-information/codes/ + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("AED"), + new TraditionalCurrency("ARS"), + new TraditionalCurrency("AUD"), + new TraditionalCurrency("BGN"), + new TraditionalCurrency("BRL"), + new TraditionalCurrency("CAD"), + new TraditionalCurrency("CHF"), + new TraditionalCurrency("CZK"), + new TraditionalCurrency("DKK"), + new TraditionalCurrency("EGP"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP"), + new TraditionalCurrency("GEL"), + new TraditionalCurrency("HUF"), + new TraditionalCurrency("ILS"), + new TraditionalCurrency("INR"), + new TraditionalCurrency("JPY"), + new TraditionalCurrency("ISK"), + new TraditionalCurrency("KWD"), + new TraditionalCurrency("KRW"), + new TraditionalCurrency("MXN"), + new TraditionalCurrency("NOK"), + new TraditionalCurrency("NZD"), + new TraditionalCurrency("PEN"), + new TraditionalCurrency("PHP"), + new TraditionalCurrency("PLN"), + new TraditionalCurrency("RON"), + new TraditionalCurrency("RSD"), + new TraditionalCurrency("RUB"), + new TraditionalCurrency("SAR"), + new TraditionalCurrency("SEK"), + new TraditionalCurrency("TRY"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("UYU") + ); + + public PaysafeAccount() { + super(PaymentMethod.PAYSAFE); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new PaysafeAccountPayload(paymentMethod.getId(), id); + } + + @Override + public @NotNull List getSupportedCurrencies() { + return SUPPORTED_CURRENCIES; + } + + @Override + public @NotNull List getInputFieldIds() { + return INPUT_FIELD_IDS; + } + + public void setEmail(String accountId) { + ((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId); + } + + public String getEmail() { + return ((PaysafeAccountPayload) paymentAccountPayload).getEmail(); + } + + @Override + protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) { + var field = super.getEmptyFormField(fieldId); + if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue(""); + return field; + } +} diff --git a/core/src/main/java/haveno/core/payment/TradeLimits.java b/core/src/main/java/haveno/core/payment/TradeLimits.java index e2de303ed5..a33ce57ca4 100644 --- a/core/src/main/java/haveno/core/payment/TradeLimits.java +++ b/core/src/main/java/haveno/core/payment/TradeLimits.java @@ -30,7 +30,9 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Singleton public class TradeLimits { - private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that. + private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that. + private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer + @Nullable @Getter private static TradeLimits INSTANCE; @@ -57,6 +59,15 @@ public class TradeLimits { return MAX_TRADE_LIMIT; } + /** + * The maximum trade limit without a buyer deposit. + * + * @return the maximum trade limit for a buyer without a deposit + */ + public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() { + return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT; + } + // We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account // age witness is not considered anymore (> 2 months). diff --git a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java index e7099879ea..297968ef0c 100644 --- a/core/src/main/java/haveno/core/payment/WeChatPayAccount.java +++ b/core/src/main/java/haveno/core/payment/WeChatPayAccount.java @@ -31,11 +31,15 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public final class WeChatPayAccount extends PaymentAccount { - public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY")); + public static final List SUPPORTED_CURRENCIES = List.of( + new TraditionalCurrency("CNY"), + new TraditionalCurrency("USD"), + new TraditionalCurrency("EUR"), + new TraditionalCurrency("GBP") + ); public WeChatPayAccount() { super(PaymentMethod.WECHAT_PAY); - setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0)); } @Override 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 cf75df3652..4a8246fe78 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -51,6 +51,7 @@ import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAtAtmAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; +import haveno.core.payment.PaysafeAccount; import haveno.core.payment.CashDepositAccount; import haveno.core.payment.CelPayAccount; import haveno.core.payment.ZelleAccount; @@ -124,13 +125,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList()); } @@ -497,17 +497,21 @@ public final class PaymentMethod implements PersistablePayload, Comparable. + */ + +package haveno.core.payment.payload; + +import com.google.protobuf.Message; +import haveno.core.locale.Res; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class PaysafeAccountPayload extends PaymentAccountPayload { + private String email = ""; + + public PaysafeAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaysafeAccountPayload(String paymentMethod, + String id, + String email, + long maxTradePeriod, + Map excludeFromJsonDataMap) { + super(paymentMethod, + id, + maxTradePeriod, + excludeFromJsonDataMap); + + this.email = email; + } + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email)) + .build(); + } + + public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new PaysafeAccountPayload(proto.getPaymentMethodId(), + proto.getId(), + proto.getPaysafeAccountPayload().getEmail(), + proto.getMaxTradePeriod(), + new HashMap<>(proto.getExcludeFromJsonDataMap())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getPaymentDetails() { + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email; + } + + @Override + public String getPaymentDetailsForTradePopup() { + return getPaymentDetails(); + } + + @Override + public byte[] getAgeWitnessInputData() { + return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8)); + } +} 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 7bf873ce4f..4545a4e210 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.getMinBuyerSecurityDepositAsPercent(); + double minPercentage = Restrictions.getMinSecurityDepositAsPercent(); 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.getMaxBuyerSecurityDepositAsPercent(); + double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent(); if (percentage > maxPercentage) return new ValidationResult(false, Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage))); diff --git a/core/src/main/java/haveno/core/proto/CoreProtoResolver.java b/core/src/main/java/haveno/core/proto/CoreProtoResolver.java index 504d59dc58..f7f8298308 100644 --- a/core/src/main/java/haveno/core/proto/CoreProtoResolver.java +++ b/core/src/main/java/haveno/core/proto/CoreProtoResolver.java @@ -54,6 +54,7 @@ import haveno.core.payment.payload.NequiAccountPayload; import haveno.core.payment.payload.OKPayAccountPayload; import haveno.core.payment.payload.PaxumAccountPayload; import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.payload.PaysafeAccountPayload; import haveno.core.payment.payload.PayPalAccountPayload; import haveno.core.payment.payload.PayseraAccountPayload; import haveno.core.payment.payload.PaytmAccountPayload; @@ -239,6 +240,8 @@ public class CoreProtoResolver implements ProtoResolver { return VenmoAccountPayload.fromProto(proto); case PAYPAL_ACCOUNT_PAYLOAD: return PayPalAccountPayload.fromProto(proto); + case PAYSAFE_ACCOUNT_PAYLOAD: + return PaysafeAccountPayload.fromProto(proto); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase); diff --git a/core/src/main/java/haveno/core/provider/ProvidersRepository.java b/core/src/main/java/haveno/core/provider/ProvidersRepository.java index f8c7da06b4..08736e0f76 100644 --- a/core/src/main/java/haveno/core/provider/ProvidersRepository.java +++ b/core/src/main/java/haveno/core/provider/ProvidersRepository.java @@ -37,6 +37,7 @@ package haveno.core.provider; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.config.Config; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -47,9 +48,12 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class ProvidersRepository { + + private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/"; private static final List DEFAULT_NODES = Arrays.asList( "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno - "http://a66ulzwhhudtqy6k2efnhodj2n6wnc5mnzjs3ocqtf47lwtcuo4wxyqd.onion/" // Cake + "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake + "http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck ); private final Config config; @@ -78,19 +82,22 @@ public class ProvidersRepository { this.providersFromProgramArgs = providers; this.useLocalhostForP2P = useLocalhostForP2P; - Collections.shuffle(DEFAULT_NODES); + Collections.shuffle(DEFAULT_NODES); // randomize order of default nodes applyBannedNodes(config.bannedPriceRelayNodes); } public void applyBannedNodes(@Nullable List bannedNodes) { this.bannedNodes = bannedNodes; + + // fill provider list fillProviderList(); - selectNextProviderBaseUrl(); + + // select next provider if current provider is null or banned + if (baseUrl.isEmpty() || isBanned(baseUrl)) selectNextProviderBaseUrl(); if (bannedNodes != null && !bannedNodes.isEmpty()) { - log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}", - bannedNodes, baseUrl, providerList); + log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}", bannedNodes, baseUrl, providerList); } } @@ -129,22 +136,30 @@ public class ProvidersRepository { // If we run in localhost mode we don't have the tor node running, so we need a clearnet host // Use localhost for using a locally running provider providers = List.of( - "http://localhost:8078/", + DEFAULT_LOCAL_NODE, "https://price.haveno.network/", "http://173.230.142.36:8078/"); } else { - providers = DEFAULT_NODES; + providers = new ArrayList(); + //providers.add(DEFAULT_LOCAL_NODE); // try local provider first + providers.addAll(DEFAULT_NODES); } } else { providers = providersFromProgramArgs; } providerList = providers.stream() - .filter(e -> bannedNodes == null || - !bannedNodes.contains(e.replace("http://", "") - .replace("/", "") - .replace(".onion", ""))) + .filter(e -> !isBanned(e)) .map(e -> e.endsWith("/") ? e : e + "/") .map(e -> e.startsWith("http") ? e : "http://" + e) .collect(Collectors.toList()); } + + private boolean isBanned(String provider) { + if (bannedNodes == null) return false; + return bannedNodes.stream() + .anyMatch(e -> provider.replace("http://", "") + .replace("/", "") + .replace(".onion", "") + .equals(e)); + } } 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 6f3566d147..c58277792f 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java @@ -292,15 +292,16 @@ public class PriceFeedService { @Nullable public MarketPrice getMarketPrice(String currencyCode) { synchronized (cache) { - return cache.getOrDefault(currencyCode, null); + return cache.getOrDefault(CurrencyUtil.getCurrencyCodeBase(currencyCode), null); } } private void setHavenoMarketPrice(String currencyCode, Price price) { UserThread.execute(() -> { + String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode); synchronized (cache) { - if (!cache.containsKey(currencyCode) || !cache.get(currencyCode).isExternallyProvidedPrice()) { - cache.put(currencyCode, new MarketPrice(currencyCode, + 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), 0, false)); 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 17bef33abb..931629473a 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceProvider.java +++ b/core/src/main/java/haveno/core/provider/price/PriceProvider.java @@ -21,6 +21,7 @@ import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import haveno.common.app.Version; import haveno.common.util.MathUtils; +import haveno.core.locale.CurrencyUtil; import haveno.core.provider.HttpClientProvider; import haveno.network.http.HttpClient; import haveno.network.p2p.P2PService; @@ -63,6 +64,7 @@ public class PriceProvider extends HttpClientProvider { String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode"); String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode"); String currencyCode = baseCurrencyCode.equals("XMR") ? counterCurrencyCode : baseCurrencyCode; + currencyCode = CurrencyUtil.getCurrencyCodeBase(currencyCode); double price = (Double) treeMap.get("price"); // json uses double for our timestampSec long value... long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index 10cbfdafaf..4bb8e86d82 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -232,10 +232,12 @@ public abstract class SupportManager { getAllChatMessages(ackMessage.getSourceId()).stream() .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .forEach(msg -> { - if (ackMessage.isSuccess()) - msg.setAcknowledged(true); - else - msg.setAckError(ackMessage.getErrorMessage()); + UserThread.execute(() -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); }); requestPersistence(); } 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 5828710d71..15595b8893 100644 --- a/core/src/main/java/haveno/core/support/dispute/Dispute.java +++ b/core/src/main/java/haveno/core/support/dispute/Dispute.java @@ -466,6 +466,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload { return this.disputeState == State.NEW; } + public boolean isOpen() { + return isNew() || this.disputeState == State.OPEN || this.disputeState == State.REOPENED; + } + public boolean isClosed() { return this.disputeState == State.CLOSED; } diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeListService.java b/core/src/main/java/haveno/core/support/dispute/DisputeListService.java index 2c90565614..c2a1521a79 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeListService.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeListService.java @@ -74,10 +74,12 @@ public abstract class DisputeListService> impleme @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(getFileName(), persisted -> { - disputeList.setAll(persisted.getList()); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + disputeList.setAll(persisted.getList()); + } + completeHandler.run(); + }, + completeHandler); } protected String getFileName() { @@ -145,26 +147,30 @@ public abstract class DisputeListService> impleme private void onDisputesChangeListener(List addedList, @Nullable List removedList) { if (removedList != null) { - removedList.forEach(dispute -> { - disputedTradeIds.remove(dispute.getTradeId()); + synchronized (removedList) { + removedList.forEach(dispute -> { + disputedTradeIds.remove(dispute.getTradeId()); + }); + } + } + synchronized (addedList) { + addedList.forEach(dispute -> { + // for each dispute added, keep track of its "BadgeCountProperty" + EasyBind.subscribe(dispute.getBadgeCountProperty(), + isAlerting -> { + // We get the event before the list gets updated, so we execute on next frame + UserThread.execute(() -> { + synchronized (disputeList.getObservableList()) { + int numAlerts = (int) disputeList.getList().stream() + .mapToLong(x -> x.getBadgeCountProperty().getValue()) + .sum(); + numOpenDisputes.set(numAlerts); + } + }); + }); + disputedTradeIds.add(dispute.getTradeId()); }); } - addedList.forEach(dispute -> { - // for each dispute added, keep track of its "BadgeCountProperty" - EasyBind.subscribe(dispute.getBadgeCountProperty(), - isAlerting -> { - // We get the event before the list gets updated, so we execute on next frame - UserThread.execute(() -> { - synchronized (disputeList.getObservableList()) { - int numAlerts = (int) disputeList.getList().stream() - .mapToLong(x -> x.getBadgeCountProperty().getValue()) - .sum(); - numOpenDisputes.set(numAlerts); - } - }); - }); - disputedTradeIds.add(dispute.getTradeId()); - }); } public void requestPersistence() { 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 eb49e0c509..ac0ede6e35 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -288,14 +288,16 @@ public abstract class DisputeManager> extends Sup cleanupDisputes(); List disputes = getDisputeList().getList(); - disputes.forEach(dispute -> { - try { - DisputeValidation.validateNodeAddresses(dispute, config); - } catch (DisputeValidation.ValidationException e) { - log.error(e.toString()); - validationExceptions.add(e); - } - }); + synchronized (disputes) { + disputes.forEach(dispute -> { + try { + DisputeValidation.validateNodeAddresses(dispute, config); + } catch (DisputeValidation.ValidationException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + }); + } maybeClearSensitiveData(); } @@ -318,11 +320,13 @@ public abstract class DisputeManager> extends Sup public void maybeClearSensitiveData() { log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName()); Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing(); - getDisputeList().getList().stream() - .filter(e -> e.isClosed()) - .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) - .forEach(Dispute::maybeClearSensitiveData); - requestPersistence(); + synchronized (getDisputeList().getList()) { + getDisputeList().getList().stream() + .filter(e -> e.isClosed()) + .filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate)) + .forEach(Dispute::maybeClearSensitiveData); + requestPersistence(); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -359,6 +363,13 @@ public abstract class DisputeManager> extends Sup return; } + // skip if payout is confirmed + if (trade.isPayoutConfirmed()) { + String errorMsg = "Cannot open dispute because payout is already confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId(); + faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg)); + return; + } + synchronized (disputeList.getObservableList()) { if (disputeList.contains(dispute)) { String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId(); @@ -368,110 +379,102 @@ public abstract class DisputeManager> extends Sup } Optional storedDisputeOptional = findDispute(dispute); - boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed(); - if (!storedDisputeOptional.isPresent() || reOpen) { + boolean reOpen = storedDisputeOptional.isPresent(); - // add or re-open dispute - if (reOpen) { - dispute = storedDisputeOptional.get(); - } else { - disputeList.add(dispute); - } - - String disputeInfo = getDisputeInfo(dispute); - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : - Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); - - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - keyRing.getPubKeyRing().hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress()); - chatMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(chatMessage); - - // create dispute opened message - trade.exportMultisigHex(); - NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); - DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType(), - trade.getSelf().getUpdatedMultisigHex(), - trade.getArbitrator().getPaymentSentMessage()); - log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); - - // send dispute opened message - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, - dispute.getAgentPubKeyRing(), - disputeOpenedMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - clearPendingMessage(); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid()); - clearPendingMessage(); - - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, - disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), - chatMessage.getUid(), errorMessage); - - clearPendingMessage(); - // We use the chatMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); - requestPersistence(); - faultHandler.handleFault("Sending dispute message failed: " + - errorMessage, new DisputeMessageDeliveryFailedException()); - } - }); + // add or re-open dispute + if (reOpen) { + dispute = storedDisputeOptional.get(); } else { - String msg = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + disputeList.add(dispute); } + + String disputeInfo = getDisputeInfo(dispute); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : + Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); + + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + keyRing.getPubKeyRing().hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + + // create dispute opened message + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType(), + trade.getSelf().getUpdatedMultisigHex(), + trade.getArbitrator().getPaymentSentMessage()); + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); + + // send dispute opened message + trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, + dispute.getAgentPubKeyRing(), + disputeOpenedMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + clearPendingMessage(); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid()); + clearPendingMessage(); + + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, + disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), + chatMessage.getUid(), errorMessage); + + clearPendingMessage(); + // We use the chatMessage wrapped inside the openNewDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + trade.setDisputeState(Trade.DisputeState.NO_DISPUTE); + requestPersistence(); + faultHandler.handleFault("Sending dispute message failed: " + + errorMessage, new DisputeMessageDeliveryFailedException()); + } + }); } requestPersistence(); @@ -531,15 +534,21 @@ public abstract class DisputeManager> extends Sup throw e; } - // try to validate payment account + // try to validate payment accounts try { - DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing + DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing } catch (Exception e) { log.error(ExceptionUtils.getStackTrace(e)); trade.prependErrorMessage(e.getMessage()); throw e; } + // set arbitrator's payment account payloads + if (trade.isArbitrator()) { + if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload()); + if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload()); + } + // get sender TradePeer sender; if (reOpen) { // re-open can come from either peer @@ -566,8 +575,9 @@ public abstract class DisputeManager> extends Sup trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); } - // update multisig hex - if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + // update opener's multisig hex + TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender; + if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex()); // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); @@ -593,7 +603,7 @@ public abstract class DisputeManager> extends Sup if (trade.isArbitrator()) { TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker(); if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress()); - sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); + sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex()); } tradeManager.requestPersistence(); errorMessage = null; diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeSession.java b/core/src/main/java/haveno/core/support/dispute/DisputeSession.java index 3b1b80e50f..5507fecc9e 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeSession.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeSession.java @@ -70,7 +70,7 @@ public abstract class DisputeSession extends SupportSession { @Override public boolean chatIsOpen() { - return dispute != null && !dispute.isClosed(); + return dispute != null && dispute.isOpen(); } @Override 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 4591a6fbc2..0905af4a1d 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java @@ -41,9 +41,12 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class DisputeValidation { - public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException { + public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException { if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId()); - if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract"); + if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract"); + if (dispute.getBuyerPaymentAccountPayload() != null) { + if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract"); + } } public static void validateDisputeData(Dispute dispute) throws ValidationException { 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 50be387c76..848dbdb6f5 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 @@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; -import haveno.core.trade.BuyerTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; @@ -177,18 +176,20 @@ public final class ArbitrationManager extends DisputeManager toRemoves = new HashSet<>(); List disputes = getDisputeList().getList(); - for (Dispute dispute : disputes) { + synchronized (disputes) { + for (Dispute dispute : disputes) { - // get dispute's trade - final Trade trade = tradeManager.getTrade(dispute.getTradeId()); - if (trade == null) { - log.warn("Dispute trade {} does not exist", dispute.getTradeId()); - return; - } - - // collect dispute if owned by arbitrator - if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) { - toRemoves.add(dispute); + // get dispute's trade + final Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) { + log.warn("Dispute trade {} does not exist", dispute.getTradeId()); + return; + } + + // collect dispute if owned by arbitrator + if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) { + toRemoves.add(dispute); + } } } for (Dispute toRemove : toRemoves) { @@ -464,14 +465,6 @@ 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"); } } else { disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex()); diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java index 6387984b20..121c1c5a3b 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationDisputeList.java @@ -55,8 +55,10 @@ public final class MediationDisputeList extends DisputeList { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() - .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } } public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java index b7fa902b83..3ae1d99501 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java @@ -196,8 +196,8 @@ public final class MediationManager extends DisputeManager tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); diff --git a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java index fb6fb2fc19..a11c6b1175 100644 --- a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java +++ b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java @@ -34,7 +34,7 @@ import java.util.Optional; public final class DisputeOpenedMessage extends DisputeMessage { private final Dispute dispute; private final NodeAddress senderNodeAddress; - private final String updatedMultisigHex; + private final String openerUpdatedMultisigHex; private final PaymentSentMessage paymentSentMessage; public DisputeOpenedMessage(Dispute dispute, @@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; - this.updatedMultisigHex = updatedMultisigHex; + this.openerUpdatedMultisigHex = updatedMultisigHex; this.paymentSentMessage = paymentSentMessage; } @@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { .setDispute(dispute.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex); + .setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); } @@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), + ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()), proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); } @@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { ",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + - ",\n updatedMultisigHex=" + updatedMultisigHex + + ",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex + ",\n paymentSentMessage=" + paymentSentMessage + "\n} " + super.toString(); } diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java index 2ad31a0107..eb041c8fac 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundDisputeList.java @@ -58,9 +58,10 @@ public final class RefundDisputeList extends DisputeList { @Override public Message toProtoMessage() { forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND")); - - return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() - .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build(); + } } public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, 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 fa3503f625..8748def337 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 @@ -196,8 +196,8 @@ public final class RefundManager extends DisputeManager { tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); @@ -205,8 +205,8 @@ public final class RefundManager extends DisputeManager { if (tradeManager.getOpenTrade(tradeId).isPresent()) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeSpentOffer(openOffer.getOffer())); } requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java index 93f03dece5..ea179a655a 100644 --- a/core/src/main/java/haveno/core/trade/ArbitratorTrade.java +++ b/core/src/main/java/haveno/core/trade/ArbitratorTrade.java @@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + /** * Trade in the context of an arbitrator. */ @@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade { String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { - super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress); + NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { + super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge); } @Override @@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java index 99fad94ebe..ca38f1ca06 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsMakerTrade.java @@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigInteger; import java.util.UUID; +import javax.annotation.Nullable; + @Slf4j public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { @@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { String uid, NodeAddress makerNodeAddress, NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress) { + NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java index 1cb7b20d47..2b9501919c 100644 --- a/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerAsTakerTrade.java @@ -44,7 +44,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/BuyerTrade.java b/core/src/main/java/haveno/core/trade/BuyerTrade.java index dbf73db101..9de8f1e03f 100644 --- a/core/src/main/java/haveno/core/trade/BuyerTrade.java +++ b/core/src/main/java/haveno/core/trade/BuyerTrade.java @@ -38,7 +38,8 @@ public abstract class BuyerTrade extends Trade { String uid, @Nullable NodeAddress takerNodeAddress, @Nullable NodeAddress makerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -47,7 +48,8 @@ public abstract class BuyerTrade extends Trade { uid, takerNodeAddress, makerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @Override diff --git a/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java b/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java index ee10d599ee..a201a985c2 100644 --- a/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java +++ b/core/src/main/java/haveno/core/trade/CleanupMailboxMessages.java @@ -55,21 +55,23 @@ public class CleanupMailboxMessages { } public void handleTrades(List trades) { - // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get - // a NullPointer and do not want that this escalate to the user. - try { - if (p2PService.isBootstrapped()) { - cleanupMailboxMessages(trades); - } else { - p2PService.addP2PServiceListener(new BootstrapListener() { - @Override - public void onDataReceived() { - cleanupMailboxMessages(trades); - } - }); + synchronized (trades) { + // We wrap in a try catch as in failed trades we cannot be sure if expected data is set, so we could get + // a NullPointer and do not want that this escalate to the user. + try { + if (p2PService.isBootstrapped()) { + cleanupMailboxMessages(trades); + } else { + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onDataReceived() { + cleanupMailboxMessages(trades); + } + }); + } + } catch (Throwable t) { + log.error("Cleanup mailbox messages failed. {}", t.toString()); } - } catch (Throwable t) { - log.error("Cleanup mailbox messages failed. {}", t.toString()); } } diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java index 0a48fc5188..02920c0e3d 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java @@ -81,13 +81,15 @@ public class ClosedTradableManager implements PersistedDataHost { @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - closedTradables.setAll(persisted.getList()); - closedTradables.stream() - .filter(tradable -> tradable.getOffer() != null) - .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + closedTradables.setAll(persisted.getList()); + closedTradables.stream() + .filter(tradable -> tradable.getOffer() != null) + .forEach(tradable -> tradable.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } public void onAllServicesInitialized() { @@ -96,7 +98,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void add(Tradable tradable) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.add(tradable)) { maybeClearSensitiveData(); requestPersistence(); @@ -105,7 +107,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void remove(Tradable tradable) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.remove(tradable)) { requestPersistence(); } @@ -117,17 +119,17 @@ public class ClosedTradableManager implements PersistedDataHost { } public ObservableList getObservableList() { - synchronized (closedTradables) { - return closedTradables.getObservableList(); - } + return closedTradables.getObservableList(); } public List getTradableList() { - return ImmutableList.copyOf(new ArrayList<>(getObservableList())); + synchronized (closedTradables.getList()) { + return ImmutableList.copyOf(new ArrayList<>(getObservableList())); + } } public List getClosedTrades() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> (Trade) e) @@ -136,7 +138,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public List getCanceledOpenOffers() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> (e instanceof OpenOffer) && ((OpenOffer) e).getState().equals(CANCELED)) .map(e -> (OpenOffer) e) @@ -145,19 +147,19 @@ public class ClosedTradableManager implements PersistedDataHost { } public Optional getTradableById(String id) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return closedTradables.stream().filter(e -> e.getId().equals(id)).findFirst(); } } - public Optional getTradeById(String id) { - synchronized (closedTradables) { - return closedTradables.stream().filter(e -> e instanceof Trade && e.getId().equals(id)).findFirst(); + public Optional getTradeById(String id) { + synchronized (closedTradables.getList()) { + return getClosedTrades().stream().filter(e -> e.getId().equals(id)).findFirst(); } } public void maybeClearSensitiveData() { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { log.info("checking closed trades eligibility for having sensitive data cleared"); closedTradables.stream() .filter(e -> e instanceof Trade) @@ -170,11 +172,11 @@ public class ClosedTradableManager implements PersistedDataHost { public boolean canTradeHaveSensitiveDataCleared(String tradeId) { Instant safeDate = getSafeDateForSensitiveDataClearing(); - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { return closedTradables.stream() - .filter(e -> e.getId().equals(tradeId)) - .filter(e -> e.getDate().toInstant().isBefore(safeDate)) - .count() > 0; + .filter(e -> e.getId().equals(tradeId)) + .filter(e -> e.getDate().toInstant().isBefore(safeDate)) + .count() > 0; } } @@ -205,9 +207,11 @@ public class ClosedTradableManager implements PersistedDataHost { } public BigInteger getTotalTradeFee(List tradableList) { - return BigInteger.valueOf(tradableList.stream() - .mapToLong(tradable -> getTradeFee(tradable).longValueExact()) - .sum()); + synchronized (tradableList) { + return BigInteger.valueOf(tradableList.stream() + .mapToLong(tradable -> getTradeFee(tradable).longValueExact()) + .sum()); + } } private BigInteger getTradeFee(Tradable tradable) { @@ -229,7 +233,7 @@ public class ClosedTradableManager implements PersistedDataHost { } public void removeTrade(Trade trade) { - synchronized (closedTradables) { + synchronized (closedTradables.getList()) { if (closedTradables.remove(trade)) { requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/Contract.java b/core/src/main/java/haveno/core/trade/Contract.java index b0950c552f..9a88eaff56 100644 --- a/core/src/main/java/haveno/core/trade/Contract.java +++ b/core/src/main/java/haveno/core/trade/Contract.java @@ -36,6 +36,7 @@ package haveno.core.trade; import com.google.protobuf.ByteString; import haveno.common.crypto.PubKeyRing; +import haveno.common.proto.ProtoUtil; import haveno.common.proto.network.NetworkPayload; import haveno.common.util.JsonExclude; import haveno.common.util.Utilities; @@ -53,6 +54,7 @@ import org.apache.commons.lang3.StringUtils; import javax.annotation.Nullable; import java.math.BigInteger; +import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; @@ -79,6 +81,7 @@ public final class Contract implements NetworkPayload { private final String makerPayoutAddressString; private final String takerPayoutAddressString; private final String makerDepositTxHash; + @Nullable private final String takerDepositTxHash; public Contract(OfferPayload offerPayload, @@ -99,7 +102,7 @@ public final class Contract implements NetworkPayload { String makerPayoutAddressString, String takerPayoutAddressString, String makerDepositTxHash, - String takerDepositTxHash) { + @Nullable String takerDepositTxHash) { this.offerPayload = offerPayload; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; @@ -134,6 +137,31 @@ public final class Contract implements NetworkPayload { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// + @Override + public protobuf.Contract toProtoMessage() { + protobuf.Contract.Builder builder = protobuf.Contract.newBuilder() + .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) + .setTradeAmount(tradeAmount) + .setTradePrice(tradePrice) + .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) + .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) + .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) + .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) + .setMakerAccountId(makerAccountId) + .setTakerAccountId(takerAccountId) + .setMakerPaymentMethodId(makerPaymentMethodId) + .setTakerPaymentMethodId(takerPaymentMethodId) + .setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash)) + .setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash)) + .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setMakerPayoutAddressString(makerPayoutAddressString) + .setTakerPayoutAddressString(takerPayoutAddressString) + .setMakerDepositTxHash(makerDepositTxHash); + Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash); + return builder.build(); + } + public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), proto.getTradeAmount(), @@ -153,32 +181,7 @@ public final class Contract implements NetworkPayload { proto.getMakerPayoutAddressString(), proto.getTakerPayoutAddressString(), proto.getMakerDepositTxHash(), - proto.getTakerDepositTxHash()); - } - - @Override - public protobuf.Contract toProtoMessage() { - return protobuf.Contract.newBuilder() - .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) - .setTradeAmount(tradeAmount) - .setTradePrice(tradePrice) - .setBuyerNodeAddress(buyerNodeAddress.toProtoMessage()) - .setSellerNodeAddress(sellerNodeAddress.toProtoMessage()) - .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) - .setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker) - .setMakerAccountId(makerAccountId) - .setTakerAccountId(takerAccountId) - .setMakerPaymentMethodId(makerPaymentMethodId) - .setTakerPaymentMethodId(takerPaymentMethodId) - .setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash)) - .setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash)) - .setMakerPubKeyRing(makerPubKeyRing.toProtoMessage()) - .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) - .setMakerPayoutAddressString(makerPayoutAddressString) - .setTakerPayoutAddressString(takerPayoutAddressString) - .setMakerDepositTxHash(makerDepositTxHash) - .setTakerDepositTxHash(takerDepositTxHash) - .build(); + ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash())); } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 3032492b89..74caf5d7f2 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.file.FileUtil; +import haveno.common.util.Base64; import haveno.common.util.Utilities; import haveno.core.api.CoreNotificationService; import haveno.core.api.XmrConnectionService; @@ -48,7 +49,10 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.PrivateKey; +import java.security.SecureRandom; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; @@ -67,6 +71,7 @@ import javax.sound.sampled.SourceDataLine; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; +import monero.common.MoneroUtils; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroTxWallet; @@ -87,13 +92,16 @@ 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 PENALTY_FEE_PCT = 0.02; // 2% + 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 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 int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase // synchronize requests to the daemon private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors? @@ -198,11 +206,11 @@ public class HavenoUtils { } public static double atomicUnitsToXmr(BigInteger atomicUnits) { - return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue(); + return MoneroUtils.atomicUnitsToXmr(atomicUnits); } public static BigInteger xmrToAtomicUnits(double xmr) { - return new BigDecimal(xmr).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger(); + return MoneroUtils.xmrToAtomicUnits(xmr); } public static long xmrToCentineros(double xmr) { @@ -214,11 +222,11 @@ public class HavenoUtils { } public static double divide(BigInteger auDividend, BigInteger auDivisor) { - return atomicUnitsToXmr(auDividend) / atomicUnitsToXmr(auDivisor); + return MoneroUtils.divide(auDividend, auDivisor); } public static BigInteger multiply(BigInteger amount1, double amount2) { - return amount1 == null ? null : new BigDecimal(amount1).multiply(BigDecimal.valueOf(amount2)).toBigInteger(); + return MoneroUtils.multiply(amount1, amount2); } // ------------------------- FORMAT UTILS --------------------------------- @@ -286,6 +294,41 @@ public class HavenoUtils { // ------------------------ SIGNING AND VERIFYING ------------------------- + public static String generateChallenge() { + 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); + + // select words randomly + List passphraseWords = new ArrayList(); + SecureRandom secureRandom = new SecureRandom(); + for (int i = 0; i < PRIVATE_OFFER_PASSPHRASE_NUM_WORDS; i++) { + passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size()))); + } + return String.join(" ", passphraseWords); + } catch (Exception e) { + throw new IllegalStateException("Failed to generate challenge", 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 + List prefixes = new ArrayList(); + for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4))); + + // hash the result + return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes())); + } + public static byte[] sign(KeyRing keyRing, String message) { return sign(keyRing.getSignatureKeyPair().getPrivate(), message); } @@ -575,14 +618,14 @@ public class HavenoUtils { // get original format AudioFormat baseFormat = audioInputStream.getFormat(); - // set target format: PCM_SIGNED, 16-bit + // set target format: PCM_SIGNED, 16-bit, 44100 Hz AudioFormat targetFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, - baseFormat.getSampleRate(), + 44100.0f, 16, // 16-bit instead of 32-bit float baseFormat.getChannels(), baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit) - baseFormat.getSampleRate(), + 44100.0f, false // Little-endian ); @@ -608,4 +651,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/SellerAsMakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java index c31c325342..07f4a16157 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsMakerTrade.java @@ -44,7 +44,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null); + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); trade.setPrice(proto.getPrice()); diff --git a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java index afca9346d1..4b5e594a1e 100644 --- a/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerAsTakerTrade.java @@ -44,7 +44,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -53,7 +54,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @@ -87,7 +89,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade uid, proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null, proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null, - proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null), + proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null, + ProtoUtil.stringOrNullFromProto(proto.getChallenge())), proto, coreProtoResolver); } diff --git a/core/src/main/java/haveno/core/trade/SellerTrade.java b/core/src/main/java/haveno/core/trade/SellerTrade.java index 457ea10aed..ddccfe59c3 100644 --- a/core/src/main/java/haveno/core/trade/SellerTrade.java +++ b/core/src/main/java/haveno/core/trade/SellerTrade.java @@ -19,6 +19,7 @@ package haveno.core.trade; import haveno.core.offer.Offer; import haveno.core.trade.protocol.ProcessModel; +import haveno.core.trade.protocol.SellerProtocol; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -36,7 +37,8 @@ public abstract class SellerTrade extends Trade { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(offer, tradeAmount, tradePrice, @@ -45,7 +47,8 @@ public abstract class SellerTrade extends Trade { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } @Override @@ -57,5 +60,9 @@ public abstract class SellerTrade extends Trade { public boolean confirmPermitted() { return true; } + + public boolean isFinished() { + return super.isFinished() && ((SellerProtocol) getProtocol()).needsToResendPaymentReceivedMessages(); + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 5f51e8eadd..aa2330a78f 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -143,7 +143,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published 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; 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; @@ -194,7 +196,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); + SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), + BUYER_RECEIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); @NotNull public Phase getPhase() { @@ -486,6 +489,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private IdlePayoutSyncer idlePayoutSyncer; @Getter private boolean isCompleted; + @Getter + private final String challenge; /////////////////////////////////////////////////////////////////////////////////////////// // Constructors @@ -500,7 +505,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { super(); this.offer = offer; this.amount = tradeAmount.longValueExact(); @@ -511,6 +517,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.uid = uid; this.takeOfferDate = new Date().getTime(); this.tradeListeners = new ArrayList(); + this.challenge = challenge; getMaker().setNodeAddress(makerNodeAddress); getTaker().setNodeAddress(takerNodeAddress); @@ -534,7 +541,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { String uid, @Nullable NodeAddress makerNodeAddress, @Nullable NodeAddress takerNodeAddress, - @Nullable NodeAddress arbitratorNodeAddress) { + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable String challenge) { this(offer, tradeAmount, @@ -544,7 +552,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); } // TODO: remove these constructors @@ -559,7 +568,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { NodeAddress arbitratorNodeAddress, XmrWalletService xmrWalletService, ProcessModel processModel, - String uid) { + String uid, + @Nullable String challenge) { this(offer, tradeAmount, @@ -569,7 +579,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { uid, makerNodeAddress, takerNodeAddress, - arbitratorNodeAddress); + arbitratorNodeAddress, + challenge); setAmount(tradeAmount); } @@ -594,12 +605,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - // notified from TradeProtocol of ack messages - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + // notified from TradeProtocol of ack messages + public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception listener.onAckMessage(ackMessage, sender); } - } + } /////////////////////////////////////////////////////////////////////////////////////////// @@ -609,8 +620,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void initialize(ProcessModelServiceProvider serviceProvider) { if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); - // done if payout unlocked and marked complete - if (isPayoutUnlocked() && isCompleted()) { + // skip initialization if trade is complete + // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox + if (isFinished()) { clearAndShutDown(); return; } @@ -625,16 +637,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); - // 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); - setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); - } + // reset states if no ack receive + if (!isPayoutPublished()) { - // 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); - setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + // 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); + 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); + resetToPaymentSentState(); + } } // handle trade state events @@ -649,6 +665,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> { if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling(); 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.PAYMENT_SENT) onPaymentSent(); if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); if (isPaymentReceived()) { @@ -693,7 +711,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { maybePublishTradeStatistics(); // reset address entries - processModel.getXmrWalletService().resetAddressEntriesForTrade(getId()); + processModel.getXmrWalletService().swapPayoutAddressEntryToAvailable(getId()); } // handle when payout unlocks @@ -722,20 +740,23 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property can become unsynced (after improper shut down?) + // 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? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); - if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) { - log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStateProperty().get()); - processModel.getPaymentSentMessageStateProperty().set(expectedState); + if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { + log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); + getSeller().getPaymentSentMessageStateProperty().set(expectedState); } } - // trade is initialized - isInitialized = true; + // handle confirmations + walletHeight.addListener((observable, oldValue, newValue) -> { + importMultisigHexIfScheduled(); + }); // done if deposit not requested or payout unlocked if (!isDepositRequested() || isPayoutUnlocked()) { + isInitialized = true; isFullyInitialized = true; return; } @@ -747,19 +768,44 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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(); + isInitialized = true; isFullyInitialized = true; return; } else { - log.warn("Missing trade wallet for {} {}, state={}, marked completed={}", getClass().getSimpleName(), getShortId(), getState(), isCompleted()); - return; + throw new RuntimeException("Missing trade wallet for " + getClass().getSimpleName() + " " + getShortId() + ", state=" + getState() + ", marked completed=" + isCompleted()); } } - // start polling if deposit requested - if (isDepositRequested()) tryInitPolling(); + // trade is initialized + isInitialized = true; + + // init syncing if deposit requested + if (isDepositRequested()) { + tryInitSyncing(); + } isFullyInitialized = true; } + public boolean isFinished() { + return isPayoutUnlocked() && isCompleted(); + } + + public void resetToPaymentSentState() { + setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + } + setPayoutTxHex(null); + } + + public void reprocessApplicableMessages() { + if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return; + getProtocol().maybeReprocessPaymentSentMessage(false); + getProtocol().maybeReprocessPaymentReceivedMessage(false); + HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + } + public void awaitInitialized() { while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling } @@ -768,6 +814,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); } + public void persistNow(@Nullable Runnable completeHandler) { + processModel.getTradeManager().persistNow(completeHandler); + } + public TradeProtocol getProtocol() { return processModel.getTradeManager().getTradeProtocol(this); } @@ -842,14 +892,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { - if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) { - onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread - return true; - } - return false; - } - public boolean isIdling() { return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -899,6 +941,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + @Override public void requestSaveWallet() { // save wallet off main thread @@ -909,6 +952,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }, getId()); } + @Override public void saveWallet() { synchronized (walletLock) { if (!walletExists()) { @@ -937,6 +981,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId()); stopPolling(); xmrWalletService.closeWallet(wallet, true); + maybeBackupWallet(); wallet = null; pollPeriodMs = null; } @@ -1064,6 +1109,34 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + public void importMultisigHexIfNeeded() { + synchronized (walletLock) { + if (wallet.isMultisigImportNeeded()) { + importMultisigHex(); + } + } + } + + public void scheduleImportMultisigHex() { + processModel.setImportMultisigHexScheduled(true); + requestPersistence(); + } + + 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; + ThreadUtils.execute(() -> { + if (!isInitialized || isShutDownStarted) return; + synchronized (getLock()) { + if (processModel.isImportMultisigHexScheduled()) { + importMultisigHex(); + processModel.setImportMultisigHexScheduled(false); + } + } + }, getId()); + } + public void importMultisigHex() { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh @@ -1078,6 +1151,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (Exception e) { log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); handleWalletError(e, sourceConnection); + doPollWallet(); + if (isPayoutPublished()) break; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1126,6 +1201,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); } + + // remove scheduled import + processModel.setImportMultisigHexScheduled(false); } catch (MoneroError e) { // import multisig hex individually if one is invalid @@ -1183,6 +1261,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // create payout tx synchronized (walletLock) { synchronized (HavenoUtils.getWalletFunctionLock()) { + + // import multisig hex if needed + importMultisigHexIfNeeded(); + + // create payout tx for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { @@ -1190,8 +1273,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); handleWalletError(e, sourceConnection); + doPollWallet(); + if (isPayoutPublished()) break; + log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1215,7 +1300,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); @@ -1250,8 +1335,10 @@ 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"); - log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); handleWalletError(e, sourceConnection); + doPollWallet(); + if (isPayoutPublished()) break; + log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1279,8 +1366,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); handleWalletError(e, sourceConnection); + doPollWallet(); + if (isPayoutPublished()) break; + log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } finally { @@ -1302,14 +1391,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroWallet wallet = getWallet(); Contract contract = getContract(); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); - BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); + BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); // describe payout tx 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 not signed + if (payoutTxId == null) updatePayout(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"); @@ -1340,6 +1429,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); 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); + // check connection boolean doSign = sign && getPayoutTxHex() == null; if (doSign || publish) verifyDaemonConnection(); @@ -1348,28 +1440,34 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (doSign) { // sign tx + String signedPayoutTxHex; try { MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null"); - setPayoutTxHex(result.getSignedMultisigTxHex()); + signedPayoutTxHex = result.getSignedMultisigTxHex(); } catch (Exception e) { throw new IllegalStateException(e); } + // verify miner fee is within tolerance unless outdated offer version + if (getOffer().getOfferPayload().getProtocolVersion() >= 2) { + + // 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()); + saveWallet(); // save wallet before creating fee estimate tx + MoneroTxWallet feeEstimateTx = createPayoutTx(); + HavenoUtils.verifyMinerFee(feeEstimateTx.getFee(), payoutTx.getFee()); + log.info("Payout tx fee {} is within tolerance"); + } + + // set signed payout tx hex + setPayoutTxHex(signedPayoutTxHex); + // describe result describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); payoutTx = describedTxSet.getTxs().get(0); updatePayout(payoutTx); - - // 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()); - 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); } // save trade state @@ -1480,7 +1578,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); - peer.setPaymentReceivedMessage(null); + if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null); } } @@ -1516,13 +1614,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // shut down trade threads - synchronized (getLock()) { - isInitialized = false; - isShutDown = true; - List shutDownThreads = new ArrayList<>(); - shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); - ThreadUtils.awaitTasks(shutDownThreads); - } + isShutDown = true; + List shutDownThreads = new ArrayList<>(); + shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); + ThreadUtils.awaitTasks(shutDownThreads); + stopProtocolTimeout(); + isInitialized = false; // save and close if (wallet != null) { @@ -1545,9 +1642,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { forceCloseWallet(); } - // backup trade wallet if applicable - maybeBackupWallet(); - // de-initialize if (idlePayoutSyncer != null) { xmrWalletService.removeWalletListener(idlePayoutSyncer); @@ -1581,7 +1675,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // unreserve maker's open offer - Optional openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId()); + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); if (this instanceof MakerTrade && openOffer.isPresent()) { processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); } @@ -1595,15 +1689,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // done if wallet already deleted if (!walletExists()) return; - // move to failed trades - processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); - // set error height if (processModel.getTradeProtocolErrorHeight() == 0) { log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getLastInfo().getHeight()); - processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); + processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); // height denotes scheduled error handling } + // move to failed trades + processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); + requestPersistence(); + // listen for deposits published to restore trade protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { if (isDepositsPublished()) { @@ -1657,12 +1752,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }); } + public boolean isProtocolErrorHandlingScheduled() { + return processModel.getTradeProtocolErrorHeight() > 0; + } + private void restoreDepositsPublishedTrade() { // close open offer - if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) { + if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) { log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId()); - processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer())); + processModel.getOpenOfferManager().closeSpentOffer(checkNotNull(getOffer())); } // re-freeze outputs @@ -1673,24 +1772,30 @@ 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(); + + // 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); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1732,6 +1837,13 @@ 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 @@ -1745,7 +1857,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.state = state; requestPersistence(); - UserThread.await(() -> { + UserThread.execute(() -> { stateProperty.set(state); phaseProperty.set(state.getPhase()); }); @@ -1777,7 +1889,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { this.payoutState = payoutState; requestPersistence(); - UserThread.await(() -> payoutStateProperty.set(payoutState)); + UserThread.execute(() -> payoutStateProperty.set(payoutState)); } public void setDisputeState(DisputeState disputeState) { @@ -1845,10 +1957,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getSeller().setPayoutTxFee(splitTxFee); getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); - } else if (getDisputeState().isClosed()) { + } else { DisputeResult disputeResult = getDisputeResult(); - if (disputeResult == null) log.warn("Dispute result is not set for {} {}", getClass().getSimpleName(), getId()); - else { + if (disputeResult != null) { BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); @@ -1992,17 +2103,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } - public MessageState getPaymentSentMessageState() { + private MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) 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: - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: return MessageState.SENT; + case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: + return MessageState.ARRIVED; case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: return MessageState.STORED_IN_MAILBOX; case SELLER_RECEIVED_PAYMENT_SENT_MSG: - return MessageState.ARRIVED; + return MessageState.ACKNOWLEDGED; case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: return MessageState.FAILED; default: @@ -2072,20 +2185,21 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); + 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?"); - long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight()); - long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); + // 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; // 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 (blockTime > now) + if (unlockTime > now) startTime = now; else - startTime = Math.max(blockTime, tradeTime); + startTime = Math.max(unlockTime, 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(blockTime)); + new Date(startTime), new Date(tradeTime), new Date(unlockTime)); } public boolean hasFailed() { @@ -2106,7 +2220,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public boolean isDepositsPublished() { if (isDepositFailed()) return false; - return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null; + return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit()); } public boolean isFundsLockedIn() { @@ -2132,7 +2246,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentSent() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG; } public boolean hasPaymentReceivedMessage() { @@ -2150,7 +2264,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentReceived() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG; } public boolean isPayoutPublished() { @@ -2258,7 +2372,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public BigInteger getTakerFee() { - return offer.getTakerFee(getAmount()); + return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount()); + } + + public BigInteger getSecurityDepositBeforeMiningFee() { + return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee(); } public BigInteger getBuyerSecurityDepositBeforeMiningFee() { @@ -2269,6 +2387,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount()); } + public boolean isBuyerAsTakerWithoutDeposit() { + return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee()); + } + + public boolean hasBuyerAsTakerWithoutDeposit() { + return getOffer().getOfferPayload().isBuyerAsTakerWithoutDeposit(); + } + @Override public BigInteger getTotalTxFee() { return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees @@ -2284,7 +2410,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isTxChainInvalid() { - return processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null; + return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit()); } /** @@ -2309,7 +2435,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return tradeAmountTransferred(); } - public boolean tradeAmountTransferred() { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean tradeAmountTransferred() { return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER); } @@ -2325,11 +2456,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) @@ -2345,7 +2471,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return tradeVolumeProperty; } - private void onConnectionChanged(MoneroRpcConnection connection) { + @Override + protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // use current connection @@ -2373,11 +2500,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // sync and reprocess messages on new thread if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) { - ThreadUtils.execute(() -> tryInitPolling(), getId()); + ThreadUtils.execute(() -> tryInitSyncing(), getId()); } } } - private void tryInitPolling() { + + private void tryInitSyncing() { if (isShutDownStarted) return; // set known deposit txs @@ -2386,23 +2514,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // start polling if (!isIdling()) { - tryInitPollingAux(); + doTryInitSyncing(); } else { long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling UserThread.runAfter(() -> ThreadUtils.execute(() -> { - if (!isShutDownStarted) tryInitPollingAux(); + if (!isShutDownStarted) doTryInitSyncing(); }, getId()), startSyncingInMs / 1000l); } } - private void tryInitPollingAux() { + private void doTryInitSyncing() { if (!wasWalletSynced) trySyncWallet(true); updatePollPeriod(); - - // reprocess pending payout messages - this.getProtocol().maybeReprocessPaymentReceivedMessage(false); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); - startPolling(); } @@ -2410,7 +2533,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { try { syncWallet(pollWallet); } catch (Exception e) { - if (!isShutDown && walletExists()) { + if (!isShutDownStarted && walletExists()) { log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } @@ -2438,9 +2561,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - if (pollWallet) pollWallet(); + if (pollWallet) doPollWallet(); } catch (Exception e) { - ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + if (!isShutDownStarted) ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); throw e; } } @@ -2500,17 +2623,25 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void doPollWallet() { + + // skip if shut down started if (isShutDownStarted) return; + + // set poll in progress + boolean pollInProgressSet = false; synchronized (pollLock) { + if (!pollInProgress) pollInProgressSet = true; pollInProgress = true; } + + // poll wallet try { // skip if payout unlocked if (isPayoutUnlocked()) return; // skip if deposit txs unknown or not requested - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if daemon not synced if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; @@ -2526,7 +2657,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // get txs from trade wallet MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); + 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); @@ -2538,22 +2669,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } setDepositTxs(txs); - if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen + if (!isPublished(getMaker().getDepositTx()) || (!hasBuyerAsTakerWithoutDeposit() && !isPublished(getTaker().getDepositTx()))) return; // skip if deposit txs not published successfully setStateDepositsSeen(); // set actual security deposits if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + 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() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed(); // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { setStateDepositsUnlocked(); } } @@ -2619,7 +2750,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } } catch (Exception e) { - if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); + if (HavenoUtils.isUnresponsive(e)) { + if (isShutDownStarted) forceCloseWallet(); + else forceRestartTradeWallet(); + } else { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { @@ -2628,13 +2762,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } } finally { - synchronized (pollLock) { - pollInProgress = false; + if (pollInProgressSet) { + synchronized (pollLock) { + pollInProgress = false; + } } - requestSaveWallet(); + saveWalletWithDelay(); } } + private static boolean isPublished(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 void syncWalletIfBehind() { synchronized (walletLock) { if (isWalletBehind()) { @@ -2721,7 +2864,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } - if (getTakerDepositTx() == null) { + if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) { log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); return true; } @@ -2743,7 +2886,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; pollWallet(); - if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); + if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitSyncing(), getId()); } private void setStateDepositsSeen() { @@ -2833,7 +2976,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // close open offer or reset address entries if (this instanceof MakerTrade) { - processModel.getOpenOfferManager().closeOpenOffer(getOffer()); + processModel.getOpenOfferManager().closeSpentOffer(getOffer()); HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation } else { getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); @@ -2843,10 +2986,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages())); } + private void onDepositsConfirmed() { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_CONFIRMED, "Trade Deposits Confirmed", "The deposit transactions have confirmed"); + } + + private void onDepositsUnlocked() { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_UNLOCKED, "Trade Deposits Unlocked", "The deposit transactions have unlocked"); + } + private void onPaymentSent() { - if (this instanceof SellerTrade) { - HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation - } + HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -2884,6 +3033,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); return builder.build(); } @@ -2953,6 +3103,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ",\n refundResultState=" + refundResultState + ",\n refundResultStateProperty=" + refundResultStateProperty + ",\n isCompleted=" + isCompleted + + ",\n challenge='" + challenge + '\'' + "\n}"; } } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 55acf63d87..9befbd6e82 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -269,13 +269,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - tradableList.setAll(persisted.getList()); - tradableList.stream() - .filter(trade -> trade.getOffer() != null) - .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + tradableList.setAll(persisted.getList()); + tradableList.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } @@ -450,17 +452,25 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi 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); - // remove trade if protocol didn't initialize - if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) { + // 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()); } } }); @@ -486,6 +496,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } // freeze or thaw outputs + if (isShutDownStarted) return; xmrWalletService.fixReservedOutputs(); // reset any available funded address entries @@ -535,6 +546,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()); @@ -549,11 +564,22 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // get open offer - Optional openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); + 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); + return; + } // ensure trade does not already exist Optional tradeOptional = getOpenTrade(request.getOfferId()); @@ -576,7 +602,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getChallenge()); else trade = new SellerAsMakerTrade(offer, BigInteger.valueOf(request.getTradeAmount()), @@ -586,7 +613,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + openOffer.getChallenge()); trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId()); trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); @@ -639,6 +667,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } + // validate challenge hash + if (offer.getChallengeHash() != null && !offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { + log.warn("Ignoring InitTradeRequest to arbitrator because challenge hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + // handle trade Trade trade; Optional tradeOptional = getOpenTrade(offer.getId()); @@ -649,7 +683,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); } @@ -672,7 +706,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi UUID.randomUUID().toString(), request.getMakerNodeAddress(), request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + request.getArbitratorNodeAddress(), + request.getChallenge()); // set reserve tx hash if available Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); @@ -725,7 +760,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -744,7 +779,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { - log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -844,67 +879,70 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi boolean isTakerApiUser, TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { + ThreadUtils.execute(() -> { + checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); - checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); - - // validate inputs - if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); - if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); - - // 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"); - - // create trade - Trade trade; - if (offer.isBuyOffer()) { - trade = new SellerAsTakerTrade(offer, - amount, - offer.getPrice().getValue(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - offer.getMakerNodeAddress(), - P2PService.getMyNodeAddress(), - null); - } else { - trade = new BuyerAsTakerTrade(offer, - amount, - offer.getPrice().getValue(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - offer.getMakerNodeAddress(), - P2PService.getMyNodeAddress(), - null); - } - trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); - trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); - trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); - trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); - trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); - trade.getSelf().setPaymentAccountId(paymentAccountId); - trade.getSelf().setPaymentMethodId(user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId()); - - // initialize trade protocol - TradeProtocol tradeProtocol = createTradeProtocol(trade); - addTrade(trade); - - initTradeAndProtocol(trade, tradeProtocol); - trade.addInitProgressStep(); - - // process with protocol - ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { - tradeResultHandler.handleResult(trade); + // validate inputs + if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); + if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); + + // 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"); + + // create trade + Trade trade; + if (offer.isBuyOffer()) { + trade = new SellerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null, + offer.getChallenge()); + } else { + trade = new BuyerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null, + offer.getChallenge()); + } + trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); + trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); + trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); + trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); + trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); + trade.getSelf().setPaymentAccountId(paymentAccountId); + trade.getSelf().setPaymentMethodId(user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId()); + + // initialize trade protocol + TradeProtocol tradeProtocol = createTradeProtocol(trade); + addTrade(trade); + + initTradeAndProtocol(trade, tradeProtocol); + trade.addInitProgressStep(); + + // process with protocol + ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { + tradeResultHandler.handleResult(trade); + requestPersistence(); + }, errorMessage -> { + log.warn("Taker error during trade initialization: " + errorMessage); + trade.onProtocolError(); + xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling + errorMessageHandler.handleErrorMessage(errorMessage); + }); + requestPersistence(); - }, errorMessage -> { - log.warn("Taker error during trade initialization: " + errorMessage); - xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error - trade.onProtocolError(); - errorMessageHandler.handleErrorMessage(errorMessage); - }); - - requestPersistence(); + }, offer.getId()); } private ProcessModel getNewProcessModel(Offer offer) { @@ -951,15 +989,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi 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.resetAddressEntriesForTrade(trade.getId()); + 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); removeFailedTrade(trade); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that. requestPersistence(); } @@ -967,7 +1005,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.info("TradeManager.removeTrade() " + trade.getId()); // remove trade - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (!tradableList.remove(trade)) return; } @@ -986,8 +1024,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); trade.setDisputeState(disputeState); - onTradeCompleted(trade); - xmrWalletService.resetAddressEntriesForTrade(trade.getId()); + xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); requestPersistence(); } } @@ -1012,8 +1049,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void updateTradePeriodState() { if (isShutDownStarted) return; - for (Trade trade : new ArrayList(tradableList.getList())) { - if (!trade.isPayoutPublished()) { + synchronized (tradableList.getList()) { + for (Trade trade : tradableList.getList()) { + if (!trade.isInitialized() || trade.isPayoutPublished()) continue; Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); if (maxTradePeriodDate != null && halfTradePeriodDate != null) { @@ -1059,13 +1097,17 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void addTradeToPendingTrades(Trade trade) { if (!trade.isInitialized()) { - initPersistedTrade(trade); + try { + initPersistedTrade(trade); + } catch (Exception e) { + log.warn("Error initializing {} {} on move to pending trades", trade.getClass().getSimpleName(), trade.getShortId(), e); + } } addTrade(trade); } public Stream getTradesStreamWithFundsLockedIn() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return getObservableList().stream().filter(Trade::isFundsLockedIn); } } @@ -1080,7 +1122,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public Set getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { AtomicReference tradeTxException = new AtomicReference<>(); - synchronized (tradableList) { + synchronized (tradableList.getList()) { Set tradesIdSet = getTradesStreamWithFundsLockedIn() .filter(Trade::hasFailed) .map(Trade::getId) @@ -1116,7 +1158,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("We found a closed trade with locked up funds. " + "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); } - } else { + } else if (!trade.hasBuyerAsTakerWithoutDeposit()) { log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } @@ -1142,7 +1184,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi initPersistedTrade(trade); UserThread.execute(() -> { - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (!tradableList.contains(trade)) { tradableList.add(trade); } @@ -1170,7 +1212,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(); @@ -1181,7 +1223,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi sourceUid, tradeId, result, - errorMessage); + errorMessage, + updatedMultisigHex); // send ack message log.info("Send AckMessage for {} to peer {}. tradeId={}, sourceUid={}", @@ -1213,7 +1256,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public ObservableList getObservableList() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.getObservableList(); } } @@ -1240,33 +1283,43 @@ 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) { + synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } } public boolean hasOpenTrade(Trade trade) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.contains(trade); } } + public boolean hasFailedScheduledTrade(String offerId) { + return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); + } + public Optional getOpenTradeByUid(String tradeUid) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst(); } } public List getAllTrades() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { List trades = new ArrayList(); - trades.addAll(tradableList.getList()); + synchronized (tradableList.getList()) { + trades.addAll(tradableList.getList()); + } trades.addAll(closedTradableManager.getClosedTrades()); trades.addAll(failedTradesManager.getObservableList()); return trades; @@ -1274,7 +1327,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public List getOpenTrades() { - synchronized (tradableList) { + synchronized (tradableList.getList()) { return ImmutableList.copyOf(getObservableList().stream() .filter(e -> e instanceof Trade) .map(e -> e) @@ -1287,7 +1340,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public Optional getClosedTrade(String tradeId) { - return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); + return closedTradableManager.getTradeById(tradeId); } public Optional getFailedTrade(String tradeId) { @@ -1295,7 +1348,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void addTrade(Trade trade) { - synchronized (tradableList) { + synchronized (tradableList.getList()) { if (tradableList.add(trade)) { requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java b/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java index a99c28242e..a188e6638e 100644 --- a/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/haveno/core/trade/failed/FailedTradesManager.java @@ -70,13 +70,15 @@ public class FailedTradesManager implements PersistedDataHost { @Override public void readPersisted(Runnable completeHandler) { persistenceManager.readPersisted(persisted -> { - failedTrades.setAll(persisted.getList()); - failedTrades.stream() - .filter(trade -> trade.getOffer() != null) - .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); - completeHandler.run(); - }, - completeHandler); + synchronized (persisted.getList()) { + failedTrades.setAll(persisted.getList()); + failedTrades.stream() + .filter(trade -> trade.getOffer() != null) + .forEach(trade -> trade.getOffer().setPriceFeedService(priceFeedService)); + } + completeHandler.run(); + }, + completeHandler); } public void onAllServicesInitialized() { @@ -84,7 +86,7 @@ public class FailedTradesManager implements PersistedDataHost { } public void add(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (failedTrades.add(trade)) { requestPersistence(); } @@ -92,7 +94,7 @@ public class FailedTradesManager implements PersistedDataHost { } public void removeTrade(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (failedTrades.remove(trade)) { requestPersistence(); } @@ -104,26 +106,26 @@ public class FailedTradesManager implements PersistedDataHost { } public ObservableList getObservableList() { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.getObservableList(); } } public Optional getTradeById(String id) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.stream().filter(e -> e.getId().equals(id)).findFirst(); } } public Stream getTradesStreamWithFundsLockedIn() { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { return failedTrades.stream() .filter(Trade::isFundsLockedIn); } } public void unFailTrade(Trade trade) { - synchronized (failedTrades) { + synchronized (failedTrades.getList()) { if (unFailTradeCallback == null) return; diff --git a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java index c743595ed4..b0ac60af85 100644 --- a/core/src/main/java/haveno/core/trade/messages/DepositRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/DepositRequest.java @@ -33,7 +33,9 @@ import java.util.Optional; public final class DepositRequest extends TradeMessage implements DirectMessage { private final long currentDate; private final byte[] contractSignature; + @Nullable private final String depositTxHex; + @Nullable private final String depositTxKey; @Nullable private final byte[] paymentAccountKey; @@ -43,8 +45,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage String messageVersion, long currentDate, byte[] contractSignature, - String depositTxHex, - String depositTxKey, + @Nullable String depositTxHex, + @Nullable String depositTxKey, @Nullable byte[] paymentAccountKey) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -63,13 +65,12 @@ public final class DepositRequest extends TradeMessage implements DirectMessage public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder() .setTradeId(offerId) - .setUid(uid) - .setDepositTxHex(depositTxHex) - .setDepositTxKey(depositTxKey); + .setUid(uid); builder.setCurrentDate(currentDate); Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex); + Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey); Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e))); - return getNetworkEnvelopeBuilder().setDepositRequest(builder).build(); } @@ -81,8 +82,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage messageVersion, proto.getCurrentDate(), ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()), - proto.getDepositTxHex(), - proto.getDepositTxKey(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()), ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey())); } diff --git a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java index 4846487598..817162addd 100644 --- a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java @@ -58,6 +58,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag private final String reserveTxKey; @Nullable private final String payoutAddress; + @Nullable + private final String challenge; public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, String offerId, @@ -79,7 +81,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag @Nullable String reserveTxHash, @Nullable String reserveTxHex, @Nullable String reserveTxKey, - @Nullable String payoutAddress) { + @Nullable String payoutAddress, + @Nullable String challenge) { super(messageVersion, offerId, uid); this.tradeProtocolVersion = tradeProtocolVersion; this.tradeAmount = tradeAmount; @@ -99,6 +102,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.payoutAddress = payoutAddress; + this.challenge = challenge; } @@ -129,6 +133,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); + Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge)); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); @@ -158,7 +163,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), - ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress())); + ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), + ProtoUtil.stringOrNullFromProto(proto.getChallenge())); } @Override @@ -183,6 +189,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + + ",\n challenge=" + challenge + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java index 8945904421..ab82e13dec 100644 --- a/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/SignContractRequest.java @@ -35,7 +35,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes private final String accountId; private final byte[] paymentAccountPayloadHash; private final String payoutAddress; + @Nullable private final String depositTxHash; + @Nullable private final byte[] accountAgeWitnessSignatureOfDepositHash; public SignContractRequest(String tradeId, @@ -45,7 +47,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes String accountId, byte[] paymentAccountPayloadHash, String payoutAddress, - String depositTxHash, + @Nullable String depositTxHash, @Nullable byte[] accountAgeWitnessSignatureOfDepositHash) { super(messageVersion, tradeId, uid); this.currentDate = currentDate; @@ -68,10 +70,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes .setUid(uid) .setAccountId(accountId) .setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash)) - .setPayoutAddress(payoutAddress) - .setDepositTxHash(depositTxHash); - + .setPayoutAddress(payoutAddress); Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e))); + Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build(); @@ -87,7 +88,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes proto.getAccountId(), proto.getPaymentAccountPayloadHash().toByteArray(), proto.getPayoutAddress(), - proto.getDepositTxHash(), + ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash())); } diff --git a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java index 98b3f1ab0d..b25c6c68d1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java @@ -43,7 +43,7 @@ public class ArbitratorProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println("ArbitratorProtocol.handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -78,7 +78,7 @@ public class ArbitratorProtocol extends DisputeProtocol { } public void handleDepositRequest(DepositRequest request, NodeAddress sender) { - System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId()); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleDepositRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 160e1bee6c..4d29a8fb42 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -60,30 +60,30 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); - ThreadUtils.execute(() -> { - synchronized (trade.getLock()) { - latchTrade(); - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessInitTradeRequest.class, - MakerSendInitTradeRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(); - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - handleTaskRunnerFault(peer, message, errorMessage); - })) - .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + latchTrade(); + this.errorMessageHandler = errorMessageHandler; + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessInitTradeRequest.class, + MakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + handleTaskRunnerFault(peer, message, errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java index 7a5a899e87..927997e611 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); 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 06e1eead7c..4302f6db6f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -119,7 +119,7 @@ public class BuyerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println("BuyerProtocol.onPaymentSent()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); 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 54aaa65d37..4f5ce9e66a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -44,6 +44,7 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.network.MessageState; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; @@ -73,6 +74,9 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; import java.util.Optional; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not @@ -90,6 +94,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private ProcessModelServiceProvider provider; transient private TradeManager tradeManager; transient private Offer offer; + transient public Throwable error; // Added in v1.4.0 // MessageState of the last message sent from the seller to the buyer in the take offer process. @@ -158,15 +163,14 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private long tradeProtocolErrorHeight; - - // We want to indicate the user the state of the message delivery of the - // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. - // To enable that even after restart we persist the state. + @Getter @Setter - private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); - @Setter - private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private boolean importMultisigHexScheduled; private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); @@ -188,6 +192,31 @@ public class ProcessModel implements Model, PersistablePayload { this.offer = offer; this.provider = provider; this.tradeManager = tradeManager; + for (TradePeer peer : getTradePeers()) { + peer.applyTransient(tradeManager); + } + + // migrate deprecated fields to new model for v1.0.19 + if (paymentSentMessageStatePropertySeller.get() != MessageState.UNDEFINED && getSeller().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getSeller().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertySeller.get()); + tradeManager.requestPersistence(); + } + if (paymentSentMessageStatePropertyArbitrator.get() != MessageState.UNDEFINED && getArbitrator().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getArbitrator().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertyArbitrator.get()); + tradeManager.requestPersistence(); + } + } + + private List getTradePeers() { + return Arrays.asList(maker, taker, arbitrator); + } + + private TradePeer getBuyer() { + return offer.getDirection() == OfferDirection.BUY ? maker : taker; + } + + private TradePeer getSeller() { + return offer.getDirection() == OfferDirection.BUY ? taker : maker; } @@ -203,11 +232,12 @@ public class ProcessModel implements Model, PersistablePayload { .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUseSavingsWallet(useSavingsWallet) .setFundsNeededForTrade(fundsNeededForTrade) - .setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()) + .setPaymentSentMessageStateSeller(paymentSentMessageStatePropertySeller.get().name()) .setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name()) .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) - .setTradeProtocolErrorHeight(tradeProtocolErrorHeight); + .setTradeProtocolErrorHeight(tradeProtocolErrorHeight) + .setImportMultisigHexScheduled(importMultisigHexScheduled); 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())); @@ -231,6 +261,7 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight()); + processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled()); // nullable processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); @@ -240,14 +271,13 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); - String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); - MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); - processModel.setPaymentSentMessageState(paymentSentMessageState); - + // deprecated fields need to be read in order to migrate to new fields + String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); + MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); + processModel.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); - processModel.setPaymentSentMessageStateArbitrator(paymentSentMessageStateArbitrator); - + processModel.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateArbitrator); return processModel; } @@ -274,38 +304,14 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessage(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageState(messageState); - } - - void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateArbitrator(messageState); - } - - public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public void setPaymentSentMessageStateArbitrator(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } + public boolean isPaymentReceivedMessagesReceived() { + return getArbitrator().isPaymentReceivedMessageReceived() && getBuyer().isPaymentReceivedMessageReceived(); } void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : - MessageState.FAILED; + MessageState.NACKED; setDepositTxMessageState(messageState); } diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java index 15d92ff785..9219d0ad7d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java @@ -65,7 +65,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java index 2332ca2003..f4914efe60 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); 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 4de8fa0d6a..2a01c5a2ca 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -53,6 +53,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { + enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -69,31 +70,37 @@ public class SellerProtocol extends DisputeProtocol { // re-send payment received message if payout not published ThreadUtils.execute(() -> { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; + if (!needsToResendPaymentReceivedMessages()) return; synchronized (trade.getLock()) { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; - if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) { - latchTrade(); - given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) - .with(SellerEvent.STARTUP)) - .setup(tasks( - SellerSendPaymentReceivedMessageToBuyer.class, - SellerSendPaymentReceivedMessageToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - unlatchTrade(); - }, - (errorMessage) -> { - log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); - unlatchTrade(); - }))) - .executeTasks(); - awaitTradeLatch(); - } + if (!needsToResendPaymentReceivedMessages()) return; + latchTrade(); + given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) + .with(SellerEvent.STARTUP)) + .setup(tasks( + SellerSendPaymentReceivedMessageToBuyer.class, + SellerSendPaymentReceivedMessageToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + unlatchTrade(); + }, + (errorMessage) -> { + log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); + unlatchTrade(); + }))) + .executeTasks(); + awaitTradeLatch(); } }, 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) { super.onTradeMessage(message, peer); @@ -115,7 +122,7 @@ public class SellerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - log.info("SellerProtocol.onPaymentReceived()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -137,7 +144,7 @@ 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.setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + trade.resetToPaymentSentState(); handleTaskRunnerFault(event, errorMessage); }))) .run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) 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 b076826b95..7ec33716a1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -24,12 +24,17 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.account.witness.AccountAgeWitness; +import haveno.core.network.MessageState; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.messages.DisputeClosedMessage; +import haveno.core.trade.TradeManager; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -57,6 +62,7 @@ public final class TradePeer implements PersistablePayload { @Nullable transient private byte[] preparedDepositTx; transient private MoneroTxWallet depositTx; + transient private TradeManager tradeManager; // Persistable mutable @Nullable @@ -96,7 +102,6 @@ public final class TradePeer implements PersistablePayload { @Getter private DisputeClosedMessage disputeClosedMessage; - // added in v 0.6 @Nullable private byte[] accountAgeWitnessNonce; @@ -142,13 +147,32 @@ public final class TradePeer implements PersistablePayload { private long payoutAmount; @Nullable private String updatedMultisigHex; - @Getter + @Deprecated + private boolean depositsConfirmedMessageAcked; + + // We want to indicate the user the state of the message delivery of the payment + // confirmation messages. We do an automatic re-send in case it was not ACKed yet. + // To enable that even after restart we persist the state. @Setter - boolean depositsConfirmedMessageAcked; + private ObjectProperty depositsConfirmedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentReceivedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); public TradePeer() { } + public void applyTransient(TradeManager tradeManager) { + this.tradeManager = tradeManager; + + // migrate deprecated fields to new model for v1.0.19 + if (depositsConfirmedMessageAcked && depositsConfirmedMessageStateProperty.get() == MessageState.UNDEFINED) { + depositsConfirmedMessageStateProperty.set(MessageState.ACKNOWLEDGED); + tradeManager.requestPersistence(); + } + } + public BigInteger getDepositTxFee() { return BigInteger.valueOf(depositTxFee); } @@ -158,7 +182,6 @@ public final class TradePeer implements PersistablePayload { } public BigInteger getSecurityDeposit() { - if (depositTxHash == null) return null; return BigInteger.valueOf(securityDeposit); } @@ -182,6 +205,60 @@ public final class TradePeer implements PersistablePayload { this.payoutAmount = payoutAmount.longValueExact(); } + void setDepositsConfirmedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.NACKED; + setDepositsConfirmedMessageState(messageState); + } + + void setPaymentSentAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.NACKED; + setPaymentSentMessageState(messageState); + } + + void setPaymentReceivedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.NACKED; + setPaymentReceivedMessageState(messageState); + } + + public void setDepositsConfirmedMessageState(MessageState depositsConfirmedMessageStateProperty) { + this.depositsConfirmedMessageStateProperty.set(depositsConfirmedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { + this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentReceivedMessageState(MessageState paymentReceivedMessageStateProperty) { + this.paymentReceivedMessageStateProperty.set(paymentReceivedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public boolean isDepositsConfirmedMessageAcked() { + return depositsConfirmedMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentSentMessageAcked() { + return paymentSentMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentReceivedMessageReceived() { + return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX || paymentReceivedMessageStateProperty.get() == MessageState.NACKED; + } + @Override public Message toProtoMessage() { final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); @@ -222,6 +299,9 @@ public final class TradePeer implements PersistablePayload { Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee)); Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount)); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); + builder.setDepositsConfirmedMessageState(depositsConfirmedMessageStateProperty.get().name()); + builder.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()); + builder.setPaymentReceivedMessageState(paymentReceivedMessageStateProperty.get().name()); builder.setCurrentDate(currentDate); return builder.build(); @@ -271,6 +351,19 @@ public final class TradePeer implements PersistablePayload { tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex())); tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee())); tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount())); + + String depositsConfirmedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDepositsConfirmedMessageState()); + MessageState depositsConfirmedMessageState = ProtoUtil.enumFromProto(MessageState.class, depositsConfirmedMessageStateString); + tradePeer.setDepositsConfirmedMessageState(depositsConfirmedMessageState); + + String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); + MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); + tradePeer.setPaymentSentMessageState(paymentSentMessageState); + + String paymentReceivedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentReceivedMessageState()); + MessageState paymentReceivedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentReceivedMessageStateString); + tradePeer.setPaymentReceivedMessageState(paymentReceivedMessageState); + return tradePeer; } } 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 684dfb7c62..5ff09324fd 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,7 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; import haveno.common.taskrunner.Task; +import haveno.core.network.MessageState; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -96,6 +97,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D 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 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 protected final ProcessModel processModel; protected final Trade trade; @@ -106,6 +108,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected ErrorMessageHandler errorMessageHandler; private boolean depositsConfirmedTasksCalled; + private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; /////////////////////////////////////////////////////////////////////////////////////////// @@ -124,12 +127,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } private void handle(TradeMessage message, NodeAddress peerNodeAddress) { @@ -163,7 +166,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } else if (networkEnvelope instanceof AckMessage) { onAckMessage((AckMessage) networkEnvelope, peer); - trade.onAckMessage((AckMessage) networkEnvelope, peer); // notify trade listeners } } @@ -208,11 +210,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); } else if (mailboxMessage instanceof AckMessage) { AckMessage ackMessage = (AckMessage) mailboxMessage; - if (!trade.isCompleted()) { - // We only apply the msg if we have not already completed the trade - onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); - } - // In any case we remove the msg + onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage); log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName()); } @@ -240,7 +238,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onInitialized() { // listen for direct messages unless completed - if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this); + if (!trade.isFinished()) processModel.getP2PService().addDecryptedDirectMessageListener(this); // initialize trade synchronized (trade.getLock()) { @@ -250,6 +248,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this); handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); + + // reprocess applicable messages + trade.reprocessApplicableMessages(); } // send deposits confirmed message if applicable @@ -257,12 +258,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void maybeSendDepositsConfirmedMessages() { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down ThreadUtils.execute(() -> { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return; depositsConfirmedTasksCalled = true; synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down + if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); expect(new Condition(trade)) .setup(tasks(getDepositsConfirmedTasks()) @@ -271,7 +273,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(); @@ -279,24 +281,42 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } - public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess - if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { return; } - log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Reprocessing PaymentSentMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); + handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError); + } + }, trade.getId()); + } + + public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { + if (trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; + synchronized (trade.getLock()) { + + // skip if no need to reprocess + if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + return; + } + + log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -333,7 +353,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -376,7 +396,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -422,7 +442,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -437,9 +457,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); @@ -452,7 +482,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(DepositsConfirmedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -481,52 +511,86 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); - if (!trade.isInitialized() || trade.isShutDown()) return; + handle(message, peer, true); + } + + // received by seller and arbitrator + protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { + log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); + + // ignore if not seller or arbitrator if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); 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.isShutDown()) 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; + + // validate signature + try { + HavenoUtils.verifyPaymentSentMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentSentMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } + + // save message for reprocessing + trade.getBuyer().setPaymentSentMessage(message); + 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; + } + 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(); } - 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(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) - .with(message) - .from(peer)) - .setup(tasks( - ApplyFilter.class, - ProcessPaymentSentMessage.class, - VerifyPeersAccountAgeWitness.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - (errorMessage) -> { - handleTaskRunnerFault(peer, message, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + }, trade.getId()); + }); } // received by buyer and arbitrator @@ -535,61 +599,83 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); - if (!trade.isInitialized() || trade.isShutDown()) return; - ThreadUtils.execute(() -> { - if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { - log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); - return; - } - synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDown()) return; - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); + log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); - // 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; - } + // ignore if not buyer or arbitrator + if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); + 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(); + // validate signature + try { + HavenoUtils.verifyPaymentReceivedMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentReceivedMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } - // 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()); + // save message for reprocessing + trade.getSeller().setPaymentReceivedMessage(message); + trade.persistNow(() -> { + + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + if (!trade.isInitialized() || trade.isShutDownStarted()) return; + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) { + log.warn("Received another PaymentReceivedMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); + handleTaskRunnerSuccess(peer, message); + 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, + () -> { + 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, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // otherwise send nack + } + unlatchTrade(); + }))) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); + }); } public void onWithdrawCompleted() { @@ -610,7 +696,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerFault(null, null, result.name(), - result.getInfo()); + result.getInfo(), + null); } }); } @@ -649,51 +736,137 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - // handle ack 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()) { - processModel.setPaymentSentAckMessage(ackMessage); - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { - processModel.setPaymentSentAckMessageArbitrator(ackMessage); - } else if (!ackMessage.isSuccess()) { - String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + " from "+ sender + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage(); - log.warn(err); - return; // log error and ignore nack if not seller - } + // ignore if trade is completely finished + if (trade.isFinished()) return; + + // get trade peer + TradePeer peer = trade.getTradePeer(sender); + if (peer == null) { + if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); + } + if (peer == null) { + if (ackMessage.isSuccess()) log.warn("Received AckMessage from unknown peer for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + else log.warn("Received AckMessage with error state from unknown peer for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + return; } - if (ackMessage.isSuccess()) { - log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + // update sender's node address + if (!peer.getNodeAddress().equals(sender)) { + log.info("Updating peer's node address from {} to {} using ACK message to {}", peer.getNodeAddress(), sender, ackMessage.getSourceMsgClassName()); + peer.setNodeAddress(sender); + } - // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time - if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { - TradePeer peer = trade.getTradePeer(sender); - if (peer == null) { - - // get the applicable peer based on the sourceUid - if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); - } - if (peer == null) log.warn("Received AckMesage for DepositsConfirmedMessage for unknown peer: " + sender); - else peer.setDepositsConfirmedMessageAcked(true); - } - } 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()); - - // set trade state on deposit request nack - if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + // handle nack of deposit request + if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); processModel.getTradeManager().requestPersistence(); } - - handleError(ackMessage.getErrorMessage()); } + + // 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 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()) { + 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()) { + 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()); + return; + } + } + + // handle ack message for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { + + // ack message from buyer + if (trade.getTradePeer(sender) == trade.getBuyer()) { + trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); + + // handle successful ack + if (ackMessage.isSuccess()) { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); + } + + // handle nack + else { + log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}", trade.getClass().getSimpleName(), trade.getId()); + + // nack includes updated multisig hex since v1.1.1 + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } + } + } + processModel.getTradeManager().requestPersistence(); + } + + // ack message from arbitrator + else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); + + // handle nack + if (!ackMessage.isSuccess()) { + log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}", trade.getClass().getSimpleName(), trade.getId()); + + // nack includes updated multisig hex since v1.1.1 + if (ackMessage.getUpdatedMultisigHex() != null) { + trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); + + // reset state if not processed + if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) { + log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId()); + trade.resetToPaymentSentState(); + } + } + } + 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()); + return; + } + } + + // generic handling + if (ackMessage.isSuccess()) { + 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("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 boolean isPaymentReceivedMessageAckedByEither() { + if (trade.getBuyer().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; + if (trade.getArbitrator().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true; + return false; } 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); @@ -703,7 +876,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); } @@ -724,7 +897,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - protected synchronized void stopTimeout() { + public synchronized void stopTimeout() { synchronized (timeoutTimerLock) { if (timeoutTimer != null) { timeoutTimer.stop(); @@ -750,11 +923,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); } @@ -816,11 +989,11 @@ 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); + sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex); } handleError(errorMessage); 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 54ced82510..c11ed57ee4 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 @@ -36,14 +36,14 @@ import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroTx; import java.math.BigInteger; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; @Slf4j public class ArbitratorProcessDepositRequest extends TradeTask { - private Throwable error; private boolean depositResponsesSent; @SuppressWarnings({"unused"}) @@ -67,7 +67,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { processDepositRequest(); complete(); } catch (Throwable t) { - this.error = t; + trade.getProcessModel().error = t; log.error("Error processing deposit request for trade {}: {}\n", trade.getId(), t.getMessage(), t); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); failed(t); @@ -83,72 +83,98 @@ public class ArbitratorProcessDepositRequest extends TradeTask { byte[] signature = request.getContractSignature(); // get trader info - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - if (trader == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); - PubKeyRing peerPubKeyRing = trader.getPubKeyRing(); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator"); + PubKeyRing senderPubKeyRing = sender.getPubKeyRing(); // verify signature - if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) { + if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) { throw new RuntimeException("Peer's contract signature is invalid"); } // set peer's signature - trader.setContractSignature(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 = trader == trade.getTaker(); - boolean isFromBuyer = trader == trade.getBuyer(); + 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(); String depositAddress = processModel.getMultisigAddress(); + sender.setSecurityDeposit(securityDeposit); // verify deposit tx - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyDepositTx( - offer.getId(), - tradeFee, - trade.getProcessModel().getTradeFeeAddress(), - sendTradeAmount, - securityDeposit, - depositAddress, - trader.getDepositTxHash(), - request.getDepositTxHex(), - request.getDepositTxKey(), - null); - } catch (Exception e) { - throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { + MoneroTx verifiedTx; + try { + verifiedTx = trade.getXmrWalletService().verifyDepositTx( + offer.getId(), + tradeFee, + trade.getProcessModel().getTradeFeeAddress(), + sendTradeAmount, + securityDeposit, + depositAddress, + sender.getDepositTxHash(), + request.getDepositTxHex(), + request.getDepositTxKey(), + null); + } 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 - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setDepositTxFee(verifiedTx.getFee()); - trader.setDepositTxHex(request.getDepositTxHex()); - trader.setDepositTxKey(request.getDepositTxKey()); - if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey()); + if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey()); processModel.getTradeManager().requestPersistence(); - // relay deposit txs when both available + // relay deposit txs when both requests received MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); - if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) { + 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()); trade.addInitProgressStep(); + // relay deposit txs boolean depositTxsRelayed = false; + List txHashes = new ArrayList<>(); try { - // submit txs to pool but do not relay + // submit maker tx to pool but do not relay MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true); - MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true); if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult)); - if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + 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); + if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult)); + txHashes.add(processModel.getTaker().getDepositTxHash()); + } // relay txs - daemon.relayTxsByHash(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())); + daemon.relayTxsByHash(txHashes); depositTxsRelayed = true; // update trade state @@ -160,7 +186,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // flush txs from pool try { - daemon.flushTxPool(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()); + daemon.flushTxPool(txHashes); } catch (Exception e2) { log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2); } @@ -168,22 +194,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(error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : 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) log.info("Arbitrator waiting for deposit request from taker 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); } @@ -196,10 +215,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // log error if (errorMessage != null) { - log.warn("Sending deposit responses with error={}", errorMessage); - Thread.dumpStack(); + log.warn("Sending deposit responses for tradeId={}, error={}", trade.getId(), errorMessage); } - + // create deposit response DepositResponse response = new DepositResponse( trade.getOffer().getId(), @@ -216,7 +234,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(), 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 10baac8567..18e97dd466 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 @@ -53,38 +53,44 @@ public class ArbitratorProcessReserveTx extends TradeTask { TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); boolean isFromMaker = sender == trade.getMaker(); boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL; + sender = isFromMaker ? processModel.getMaker() : processModel.getTaker(); + BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + sender.setSecurityDeposit(securityDeposit); // TODO (woodser): if signer online, should never be called by maker? - // process reserve tx with expected values - BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), 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 - BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - MoneroTx verifiedTx; - try { - verifiedTx = trade.getXmrWalletService().verifyReserveTx( - offer.getId(), - penaltyFee, - tradeFee, - sendAmount, - securityDeposit, - request.getPayoutAddress(), - request.getReserveTxHash(), - request.getReserveTxHex(), - request.getReserveTxKey(), - null); - } 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()); - } + // process reserve tx unless from buyer as taker without deposit + boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit(); + if (!isFromBuyerAsTakerWithoutDeposit) { - // save reserve tx to model - TradePeer trader = isFromMaker ? processModel.getMaker() : processModel.getTaker(); - trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit - trader.setReserveTxHash(request.getReserveTxHash()); - trader.setReserveTxHex(request.getReserveTxHex()); - trader.setReserveTxKey(request.getReserveTxKey()); + // process reserve tx with expected values + BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), 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( + offer.getId(), + penaltyFee, + tradeFee, + sendAmount, + securityDeposit, + request.getPayoutAddress(), + request.getReserveTxHash(), + request.getReserveTxHex(), + request.getReserveTxKey(), + null); + } 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 processModel.getTradeManager().requestPersistence(); 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 9ec7aebe28..a846077db4 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 @@ -78,6 +78,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { null, null, null, + null, null); // send request to taker @@ -118,7 +119,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { // ensure arbitrator has reserve txs if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); - if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); + if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); // create wallet for multisig MoneroWallet multisigWallet = trade.createWallet(); 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 2cd4376ce6..05fee1374a 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 @@ -36,6 +36,7 @@ package haveno.core.trade.protocol.tasks; import com.google.common.base.Preconditions; import haveno.common.taskrunner.TaskRunner; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; @@ -73,21 +74,27 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null"); Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); - Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); + if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); // create payout tx if we have seller's updated multisig hex if (trade.getSeller().getUpdatedMultisigHex() != null) { - // import multisig hex - trade.importMultisigHex(); + // synchronize on lock for wallet operations + synchronized (trade.getWalletLock()) { + synchronized (HavenoUtils.getWalletFunctionLock()) { - // 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(); + // 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(); + } + } } complete(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index 42206c8de0..86bb957577 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -66,7 +66,7 @@ import lombok.extern.slf4j.Slf4j; public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -142,26 +142,26 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask @Override protected void setStateSent() { - if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask timer.stop(); } if (listener != null) { - processModel.getPaymentSentMessageStateProperty().removeListener(listener); + trade.getSeller().getPaymentReceivedMessageStateProperty().removeListener(listener); } } @@ -185,7 +185,6 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask return; } - log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) { timer.stop(); } @@ -194,21 +193,30 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStateProperty().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); + getReceiver().getPaymentSentMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get()); } - delayInMin = delayInMin * 2; + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } resendCounter++; } private void onMessageStateChange(MessageState newValue) { - if (newValue == MessageState.ACKNOWLEDGED) { - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); + if (isAckedByReceiver()) { cleanup(); } } - protected abstract boolean isAckedByReceiver(); + protected boolean isAckedByReceiver() { + return getReceiver().isPaymentSentMessageAcked(); + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java index cc4113e342..9fea701200 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; @@ -39,26 +38,7 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen @Override protected void setStateSent() { + super.setStateSent(); complete(); // don't wait for message to arbitrator } - - @Override - protected void setStateFault() { - // state only updated on seller message - } - - @Override - protected void setStateStoredInMailbox() { - // state only updated on seller message - } - - @Override - protected void setStateArrived() { - // state only updated on seller message - } - - @Override - protected boolean isAckedByReceiver() { - return trade.getProcessModel().getPaymentSentMessageStatePropertyArbitrator().get() == MessageState.ACKNOWLEDGED; - } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java index caf402be0a..57ca170455 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.TradePeer; @@ -40,25 +39,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected void setStateSent() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.SENT); + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); super.setStateSent(); } @Override protected void setStateArrived() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.ARRIVED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.FAILED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); super.setStateFault(); } @@ -69,9 +68,4 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); complete(); } - - @Override - protected boolean isAckedByReceiver() { - return trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal(); - } } 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 91398e82e1..9f191131ec 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 @@ -138,7 +138,8 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); + model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(), + trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), 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 69d1620aea..6f10625e35 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 @@ -19,6 +19,7 @@ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.OpenOffer; import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -35,6 +36,7 @@ import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; import java.util.Date; +import java.util.Optional; import java.util.UUID; // TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest @@ -83,12 +85,17 @@ public class MaybeSendSignContractRequest extends TradeTask { if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId()); trade.startProtocolTimeout(); - // collect relevant info + // collect info Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { - reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); - if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(trade.getId()); + if (openOffer.isPresent()) { + reserveExactAmount = openOffer.get().isReserveExactAmount(); + if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + } else { + throw new RuntimeException("Cannot request contract signature because open offer has been removed for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + } } // thaw reserved outputs @@ -97,53 +104,60 @@ public class MaybeSendSignContractRequest extends TradeTask { } // attempt creating deposit tx - try { - synchronized (HavenoUtils.getWalletFunctionLock()) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); - try { - 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); + if (!trade.isBuyerAsTakerWithoutDeposit()) { + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); + try { + 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); + 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 + } + + // check for timeout 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 + if (depositTx != null) break; } - - // check for timeout - if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); - if (depositTx != null) break; } + } catch (Exception e) { + + // thaw deposit inputs + if (depositTx != null) { + trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + + // re-freeze maker offer inputs + if (trade instanceof MakerTrade) { + trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); + } + + throw e; } - } catch (Exception e) { - - // thaw deposit inputs - if (depositTx != null) { - trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx)); - trade.getSelf().setReserveTxKeyImages(null); - } - - // re-freeze maker offer inputs - if (trade instanceof MakerTrade) { - trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages()); - } - - throw e; } // reset protocol timeout trade.addInitProgressStep(); // update trade state - BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); - trade.getSelf().setDepositTx(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - trade.getSelf().setDepositTxFee(depositTx.getFee()); - trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); + trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash()); + BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + if (depositTx == null) { + trade.getSelf().setSecurityDeposit(securityDeposit); + } else { + trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); + trade.getSelf().setDepositTx(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx)); + } } // maker signs deposit hash nonce to avoid challenge protocol @@ -161,7 +175,7 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.getProcessModel().getAccountId(), trade.getSelf().getPaymentAccountPayload().getHash(), trade.getSelf().getPayoutAddressString(), - depositTx.getHash(), + depositTx == null ? null : depositTx.getHash(), sig); // send request to trading peer 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 e37c612136..454763e15b 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,12 +38,14 @@ public class ProcessDepositResponse extends TradeTask { try { runInterceptHook(); - // throw if error + // handle error DepositResponse message = (DepositResponse) processModel.getTradeMessage(); if (message.getErrorMessage() != null) { + log.warn("Deposit response for {} {} has error message={}", 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; } // 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 c11df74fae..7e0c85af2d 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 @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.ThreadUtils; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -63,17 +62,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // update multisig hex if (sender.getUpdatedMultisigHex() == null) { sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - - // try to import multisig hex (retry later) - if (!trade.isPayoutPublished()) { - ThreadUtils.submitToPool(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - log.warn("Error importing multisig hex on deposits confirmed for trade " + trade.getId() + ": " + e.getMessage() + "\n", e); - } - }); - } + trade.scheduleImportMultisigHex(); } // persist 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 28c61d51df..5d55bbaea7 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 @@ -72,6 +72,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // update to the latest peer address of our peer if message is correct trade.getSeller().setNodeAddress(processModel.getTempTradePeerNodeAddress()); if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses + trade.requestPersistence(); // ack and complete if already processed if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) { @@ -80,8 +81,13 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } - // save message for reprocessing - trade.getSeller().setPaymentReceivedMessage(message); + // 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()); + } + } // set state trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); @@ -105,12 +111,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // advance state, arbitrator auto completes when payout published trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); - // publish signed witness + // buyer republishes signed witness for resilience SignedWitness signedWitness = message.getBuyerSignedWitness(); if (signedWitness != null && trade instanceof BuyerTrade) { - // We received the signedWitness from the seller and publish the data to the network. - // The signer has published it as well but we prefer to re-do it on our side as well to achieve higher - // resilience. processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness); } @@ -131,14 +134,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { private void processPayoutTx(PaymentReceivedMessage message) { - // adapt from 1.0.6 to 1.0.7 which changes field usage - // TODO: remove after future updates to allow old trades to clear - if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) { - log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId()); - if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex()); - trade.setPayoutTxHex(null); - } - // update wallet trade.importMultisigHex(); trade.syncAndPollWallet(); @@ -146,12 +141,10 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // handle if payout tx not published if (!trade.isPayoutPublished()) { - // wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer) - boolean isSigned = message.getSignedPayoutTxHex() != null; - boolean deferSignAndPublish = trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout(); - if (deferSignAndPublish) { - log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); - trade.pollWalletNormallyForMs(60000); + // wait to publish payout tx if defer flag set from seller (payout is expected) + if (message.isDeferPublishPayout()) { + log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); + if (trade instanceof ArbitratorTrade) trade.pollWalletNormallyForMs(60000); // stop idling arbitrator for (int i = 0; i < 5; i++) { if (trade.isPayoutPublished()) break; HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); @@ -162,6 +155,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // verify and publish payout tx if (!trade.isPayoutPublished()) { try { + boolean isSigned = message.getSignedPayoutTxHex() != null; if (isSigned) { log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId()); 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 93d1ce520a..1f99d64806 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 @@ -46,9 +46,17 @@ public class ProcessPaymentSentMessage extends TradeTask { // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); + trade.requestPersistence(); + + // cannot process until wallet sees deposits confirmed + 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()); + } + } // update state from message - trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java index 8fc93df9a9..1ce805aca6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -63,20 +63,20 @@ public class ProcessSignContractRequest extends TradeTask { // extract fields from request // TODO (woodser): verify request and from maker or taker SignContractRequest request = (SignContractRequest) processModel.getTradeMessage(); - TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - trader.setDepositTxHash(request.getDepositTxHash()); - trader.setAccountId(request.getAccountId()); - trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); - trader.setPayoutAddressString(request.getPayoutAddress()); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + sender.setDepositTxHash(request.getDepositTxHash()); + sender.setAccountId(request.getAccountId()); + sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash()); + sender.setPayoutAddressString(request.getPayoutAddress()); // maker sends witness signature of deposit tx hash - if (trader == trade.getMaker()) { - trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); - trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); + if (sender == trade.getMaker()) { + sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8)); + sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash()); } - // sign contract only when both deposit txs hashes known - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) { + // sign contract only when received from both peers + if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) { complete(); return; } 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 a483026a6e..a146fa3419 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 @@ -19,6 +19,7 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; import haveno.core.support.dispute.Dispute; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; @@ -42,41 +43,40 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { // handle first time preparation if (trade.getArbitrator().getPaymentReceivedMessage() == null) { - // adapt from 1.0.6 to 1.0.7 which changes field usage - // TODO: remove after future updates to allow old trades to clear - if (trade.getPayoutTxHex() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) { - log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId()); - trade.setPayoutTxHex(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) { - try { + // import multisig hex unless already signed 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); + 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(); } - } catch (IllegalArgumentException | IllegalStateException e) { - log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}: {}. Creating new unsigned payout tx", 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 if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) { // republish payout tx from previous message 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 fe2f1ac442..9fe31dba60 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 @@ -35,11 +35,15 @@ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; + +import haveno.common.Timer; +import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.taskrunner.TaskRunner; 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.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; @@ -47,15 +51,23 @@ import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.JsonUtil; import haveno.network.p2p.NodeAddress; +import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; +import java.util.concurrent.TimeUnit; + @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { - SignedWitness signedWitness = null; + private SignedWitness signedWitness = null; + private ChangeListener listener; + private Timer timer; + private static final int MAX_RESEND_ATTEMPTS = 20; + private int delayInMin = 10; + private int resendCounter = 0; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -77,6 +89,13 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag protected void run() { try { runInterceptHook(); + + // skip if stopped + if (stopSending()) { + if (!isCompleted()) complete(); + return; + } + super.run(); } catch (Throwable t) { failed(t); @@ -90,8 +109,12 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag // sign account witness AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); if (accountAgeWitnessService.isSignWitnessTrade(trade)) { - accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); - log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId()); + try { + accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); + log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId()); + } catch (Exception e) { + log.warn("Failed to sign and publish peer's account age witness for {} {}, error={}\n", getClass().getSimpleName(), trade.getId(), e.getMessage(), e); + } } // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the @@ -99,6 +122,7 @@ 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 PaymentReceivedMessage message = new PaymentReceivedMessage( tradeId, processModel.getMyNodeAddress(), @@ -106,7 +130,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag 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(), - trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout + deferPublishPayout, trade.getTradePeer().getAccountAgeWitness(), signedWitness, getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message @@ -129,29 +153,89 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag @Override protected void setStateSent() { - trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.SENT); + tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } + + private void cleanup() { + if (timer != null) { + timer.stop(); + } + if (listener != null) { + trade.getBuyer().getPaymentReceivedMessageStateProperty().removeListener(listener); + } + } + + private void tryToSendAgainLater() { + + // skip if stopped + if (stopSending()) return; + + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + log.warn("We never received an ACK message when sending the PaymentReceivedMessage to the peer. We stop trying to send the message."); + return; + } + + if (timer != null) { + timer.stop(); + } + + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + + if (resendCounter == 0) { + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + getReceiver().getPaymentReceivedMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentReceivedMessageStateProperty().get()); + } + + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + if (isMessageReceived()) { + cleanup(); + } + } + + protected boolean isMessageReceived() { + return getReceiver().isPaymentReceivedMessageReceived(); + } + + protected boolean stopSending() { + return isMessageReceived() || !trade.isPaymentReceived(); // stop if received or trade state reset // TODO: also stop after some number of blocks? + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java index 7228b40307..212dcb22f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java @@ -37,6 +37,30 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe return trade.getBuyer(); } + @Override + protected void setStateSent() { + trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + super.setStateSent(); + } + + @Override + protected void setStateFault() { + trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + super.setStateFault(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); + super.setStateStoredInMailbox(); + } + + @Override + protected void setStateArrived() { + trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); + super.setStateArrived(); + } + // continue execution on fault so payment received message is sent to arbitrator @Override protected void onFault(String errorMessage, TradeMessage message) { 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 dd43d6944f..7101c488a5 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().getFullHex(), - trade.getSelf().getDepositTx().getKey(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(), + trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(), trade.getSelf().getPaymentAccountKey()); // update trade state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index 8be8f00da5..ba20d74351 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -23,6 +23,7 @@ import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; +import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -37,7 +38,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask { private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -52,8 +53,8 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas try { runInterceptHook(); - // skip if already acked by receiver - if (isAckedByReceiver()) { + // skip if already acked or payout published + if (isAckedByReceiver() || trade.isPayoutPublished()) { if (!isCompleted()) complete(); return; } @@ -64,11 +65,17 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } } - @Override - protected abstract NodeAddress getReceiverNodeAddress(); + protected abstract TradePeer getReceiver(); @Override - protected abstract PubKeyRing getReceiverPubKeyRing(); + protected NodeAddress getReceiverNodeAddress() { + return getReceiver().getNodeAddress(); + } + + @Override + protected PubKeyRing getReceiverPubKeyRing() { + return getReceiver().getPubKeyRing(); + } @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { @@ -97,23 +104,24 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas @Override protected void setStateSent() { + getReceiver().setDepositsConfirmedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.ARRIVED); } @Override protected void setStateStoredInMailbox() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.STORED_IN_MAILBOX); } @Override protected void setStateFault() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.FAILED); } private void cleanup() { @@ -137,7 +145,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas timer.stop(); } - // first re-send is after 2 minutes, then double the delay each iteration + // first re-send is after 2 minutes, then increase the delay exponentially if (resendCounter == 0) { int shortDelay = 2; log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); @@ -145,13 +153,12 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } else { log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); - delayInMin = delayInMin * 2; + delayInMin = (int) ((double) delayInMin * 1.5); } resendCounter++; } private boolean isAckedByReceiver() { - TradePeer peer = trade.getTradePeer(getReceiverNodeAddress()); - return peer.isDepositsConfirmedMessageAcked(); + return getReceiver().isDepositsConfirmedMessageAcked(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java index baaa6ae987..ae8a171aa8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfir } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getArbitrator().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getArbitrator().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getArbitrator(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java index 5795ce8947..bf1212c9a8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMe } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getBuyer().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getBuyer().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getBuyer(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java index efdf9a99cd..4ea097fd2b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedM } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getSeller().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getSeller().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getSeller(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java index 157d797410..24638c5e70 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java @@ -82,6 +82,7 @@ public abstract class SendMailboxMessageTask extends TradeTask { @Override public void onFault(String errorMessage) { + if (processModel.getP2PService().isShutDownStarted()) return; log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getOfferId(), message.getUid(), errorMessage); SendMailboxMessageTask.this.onFault(errorMessage, message); } 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 5ab91343aa..9a461b2d83 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 @@ -47,62 +47,66 @@ public class TakerReserveTradeFunds extends TradeTask { throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen."); } - // create reserve tx + // create reserve tx unless deposit not required from buyer as taker MoneroTxWallet reserveTx = null; - synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + if (!trade.isBuyerAsTakerWithoutDeposit()) { + 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(); + // 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(); - // 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.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); - String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + // 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(); + String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - // attempt 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, takerFee, sendAmount, securityDeposit, returnAddress, false, null); - } 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); + // attempt 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, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, 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); + 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + if (reserveTx != null) break; } - - // 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) { + } catch (Exception e) { - // reset state with wallet lock - model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId()); - if (reserveTx != null) { - model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); - trade.getSelf().setReserveTxKeyImages(null); + // reset state with wallet lock + model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId()); + if (reserveTx != null) { + model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + trade.getSelf().setReserveTxKeyImages(null); + } + + throw e; } - throw e; + // 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)); } - - - // 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)); } // save process state diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java index b5a6e3624c..eb86f15109 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java @@ -48,7 +48,9 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask { InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker checkNotNull(sourceRequest); checkTradeId(processModel.getOfferId(), sourceRequest); - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // create request to arbitrator Offer offer = processModel.getOffer(); @@ -73,7 +75,8 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), + trade.getChallenge()); // send request to arbitrator log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java index c6315eb174..1118cc34a7 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java @@ -47,7 +47,9 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { runInterceptHook(); // verify trade state - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) { + throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + } // collect fields Offer offer = model.getOffer(); @@ -55,6 +57,7 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { P2PService p2PService = processModel.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String challenge = model.getChallenge(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId()); @@ -81,7 +84,8 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask { null, // reserve tx not sent from taker to maker null, null, - payoutAddress); + payoutAddress, + challenge); // send request to maker log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 209dc7fb7f..02b1d82aaf 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -35,6 +35,7 @@ import haveno.core.locale.TradeCurrency; import haveno.core.locale.TraditionalCurrency; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountUtil; +import haveno.core.trade.HavenoUtils; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; import haveno.core.xmr.nodes.XmrNodes.MoneroNodesOption; @@ -289,7 +290,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid setUseTorForXmr(config.useTorForXmr); // switch to public nodes if no provided nodes available - if (getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PROVIDED.ordinal() && xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(this)).isEmpty()) { + boolean isFixedConnection = !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !config.ignoreLocalXmrNode); + if (!isFixedConnection && getMoneroNodesOptionOrdinal() == XmrNodes.MoneroNodesOption.PROVIDED.ordinal() && xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(this)).isEmpty()) { log.warn("No provided nodes available, switching to public nodes"); setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); } @@ -566,6 +568,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setBuyScreenOtherCurrencyCode(String buyScreenCurrencyCode) { + prefPayload.setBuyScreenOtherCurrencyCode(buyScreenCurrencyCode); + requestPersistence(); + } + + public void setSellScreenOtherCurrencyCode(String sellScreenCurrencyCode) { + prefPayload.setSellScreenOtherCurrencyCode(sellScreenCurrencyCode); + requestPersistence(); + } + public void setIgnoreTradersList(List ignoreTradersList) { prefPayload.setIgnoreTradersList(ignoreTradersList); requestPersistence(); @@ -606,14 +618,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } - public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) { - double max = Restrictions.getMaxBuyerSecurityDepositAsPercent(); - double min = Restrictions.getMinBuyerSecurityDepositAsPercent(); + public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) { + double max = Restrictions.getMaxSecurityDepositAsPercent(); + double min = Restrictions.getMinSecurityDepositAsPercent(); if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount)) - prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent))); else - prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent))); + prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent))); requestPersistence(); } @@ -745,6 +757,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); } + public void setShowPrivateOffers(boolean value) { + prefPayload.setShowPrivateOffers(value); + requestPersistence(); + } + public void setDenyApiTaker(boolean value) { prefPayload.setDenyApiTaker(value); requestPersistence(); @@ -828,16 +845,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid return prefPayload.isSplitOfferOutput(); } - public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) { + public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) { double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ? - prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent(); + prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent(); - if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) { - value = Restrictions.getMinBuyerSecurityDepositAsPercent(); - setBuyerSecurityDepositAsPercent(value, paymentAccount); + if (value < Restrictions.getMinSecurityDepositAsPercent()) { + value = Restrictions.getMinSecurityDepositAsPercent(); + setSecurityDepositAsPercent(value, paymentAccount); } - return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value; + return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : 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 e45f12826b..44e6aef509 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.getDefaultBuyerSecurityDepositAsPercent; +import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent; @Slf4j @Data @@ -77,6 +77,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String buyScreenCryptoCurrencyCode; @Nullable private String sellScreenCryptoCurrencyCode; + @Nullable + private String buyScreenOtherCurrencyCode; + @Nullable + private String sellScreenOtherCurrencyCode; private int tradeStatisticsTickUnitIndex = 3; private boolean resyncSpvRequested; private boolean sortMarketCurrenciesNumerically = true; @@ -116,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope { private String rpcPw; @Nullable private String takeOfferSelectedPaymentAccountId; - private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent(); private int ignoreDustThreshold = 600; private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL; - private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(); + private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent(); private int blockNotifyPort; private boolean tacAcceptedV120; private double bsqAverageTrimThreshold = 0.05; @@ -130,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope { // Added in 1.5.5 private boolean hideNonAccountPaymentMethods; private boolean showOffersMatchingMyAccounts; + private boolean showPrivateOffers; private boolean denyApiTaker; private boolean notifyOnPreRelease; @@ -189,10 +194,10 @@ public final class PreferencesPayload implements PersistableEnvelope { .setUseStandbyMode(useStandbyMode) .setUseSoundForNotifications(useSoundForNotifications) .setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized) - .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) + .setSecurityDepositAsPercent(securityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setClearDataAfterDays(clearDataAfterDays) - .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) + .setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto) .setBlockNotifyPort(blockNotifyPort) .setTacAcceptedV120(tacAcceptedV120) .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) @@ -201,6 +206,7 @@ public final class PreferencesPayload implements PersistableEnvelope { .collect(Collectors.toList())) .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) + .setShowPrivateOffers(showPrivateOffers) .setDenyApiTaker(denyApiTaker) .setNotifyOnPreRelease(notifyOnPreRelease); @@ -212,6 +218,8 @@ public final class PreferencesPayload implements PersistableEnvelope { Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode); Optional.ofNullable(buyScreenCryptoCurrencyCode).ifPresent(builder::setBuyScreenCryptoCurrencyCode); Optional.ofNullable(sellScreenCryptoCurrencyCode).ifPresent(builder::setSellScreenCryptoCurrencyCode); + Optional.ofNullable(buyScreenOtherCurrencyCode).ifPresent(builder::setBuyScreenOtherCurrencyCode); + Optional.ofNullable(sellScreenOtherCurrencyCode).ifPresent(builder::setSellScreenOtherCurrencyCode); Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent( account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage())); Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses); @@ -260,6 +268,8 @@ public final class PreferencesPayload implements PersistableEnvelope { ProtoUtil.stringOrNullFromProto(proto.getSellScreenCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCryptoCurrencyCode()), ProtoUtil.stringOrNullFromProto(proto.getSellScreenCryptoCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getBuyScreenOtherCurrencyCode()), + ProtoUtil.stringOrNullFromProto(proto.getSellScreenOtherCurrencyCode()), proto.getTradeStatisticsTickUnitIndex(), proto.getResyncSpvRequested(), proto.getSortMarketCurrenciesNumerically(), @@ -289,10 +299,10 @@ public final class PreferencesPayload implements PersistableEnvelope { proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), - proto.getBuyerSecurityDepositAsPercent(), + proto.getSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), proto.getClearDataAfterDays(), - proto.getBuyerSecurityDepositAsPercentForCrypto(), + proto.getSecurityDepositAsPercentForCrypto(), proto.getBlockNotifyPort(), proto.getTacAcceptedV120(), proto.getBsqAverageTrimThreshold(), @@ -302,6 +312,7 @@ public final class PreferencesPayload implements PersistableEnvelope { .collect(Collectors.toList())), proto.getHideNonAccountPaymentMethods(), proto.getShowOffersMatchingMyAccounts(), + proto.getShowPrivateOffers(), proto.getDenyApiTaker(), proto.getNotifyOnPreRelease(), XmrNodeSettings.fromProto(proto.getXmrNodeSettings()) diff --git a/core/src/main/java/haveno/core/util/PriceUtil.java b/core/src/main/java/haveno/core/util/PriceUtil.java index 35a6b0bec8..a91b13c85c 100644 --- a/core/src/main/java/haveno/core/util/PriceUtil.java +++ b/core/src/main/java/haveno/core/util/PriceUtil.java @@ -22,7 +22,6 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import haveno.common.util.MathUtils; import haveno.core.locale.CurrencyUtil; -import haveno.core.locale.Res; import haveno.core.monetary.CryptoMoney; import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; @@ -69,27 +68,8 @@ public class PriceUtil { if (!result.isValid) { return result; } - - long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, marketPrice.getCurrencyCode()); - long marketPriceAsLong = PriceUtil.getMarketPriceAsLong("" + marketPrice.getPrice(), marketPrice.getCurrencyCode()); - String marketPriceAsString = FormattingUtils.formatMarketPrice(marketPrice.getPrice(), marketPrice.getCurrencyCode()); - - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - if ((isSellOffer && !isCryptoCurrency) || (!isSellOffer && isCryptoCurrency)) { - if (triggerPriceAsLong >= marketPriceAsLong) { - return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooHigh", marketPriceAsString)); - } else { - return new InputValidator.ValidationResult(true); - } - } else { - if (triggerPriceAsLong <= marketPriceAsLong) { - return new InputValidator.ValidationResult(false, - Res.get("createOffer.triggerPrice.invalid.tooLow", marketPriceAsString)); - } else { - return new InputValidator.ValidationResult(true); - } - } + + return new InputValidator.ValidationResult(true); } public static Price marketPriceToPrice(MarketPrice marketPrice) { 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 ec7ff113e0..6c163ede6a 100644 --- a/core/src/main/java/haveno/core/util/coin/CoinUtil.java +++ b/core/src/main/java/haveno/core/util/coin/CoinUtil.java @@ -47,46 +47,46 @@ public class CoinUtil { } /** - * @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC) + * @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR) * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger value) { - return getAsPercentPerBtc(value, HavenoUtils.xmrToAtomicUnits(1.0)); + public static double getAsPercentPerXmr(BigInteger value) { + return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0)); } /** - * @param part Btc amount to be converted to percent value, based on total value passed. - * E.g. 0.1 BTC is 25% (of 0.4 BTC) - * @param total Total Btc amount the percentage part is calculated from + * @param part Xmr amount to be converted to percent value, based on total value passed. + * E.g. 0.1 XMR is 25% (of 0.4 XMR) + * @param total Total Xmr amount the percentage part is calculated from * * @return The percentage value as double (e.g. 1% is 0.01) */ - public static double getAsPercentPerBtc(BigInteger part, BigInteger total) { + public static double getAsPercentPerXmr(BigInteger part, BigInteger total) { return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4); } /** * @param percent The percentage value as double (e.g. 1% is 0.01) * @param amount The amount as atomic units for the percentage calculation - * @return The percentage as atomic units (e.g. 1% of 1 BTC is 0.01 BTC) + * @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR) */ public static BigInteger getPercentOfAmount(double percent, BigInteger amount) { if (amount == null) amount = BigInteger.ZERO; 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) { - if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { - return getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { - return getRoundedAmountUnit(amount, price, maxTradeLimit); - } else if (CurrencyUtil.isFiatCurrency(currencyCode)) { - return getRoundedAmount4Decimals(amount, price, maxTradeLimit); + public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) { + if (price != null) { + if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) { + return getRoundedAtmCashAmount(amount, price, maxTradeLimit); + } else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) { + return getRoundedAmountUnit(amount, price, maxTradeLimit); + } } - return amount; + return getRoundedAmount4Decimals(amount, maxTradeLimit); } - public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 10); } @@ -99,12 +99,12 @@ public class CoinUtil { * @param maxTradeLimit The max. trade limit of the users account, in atomic units. * @return The adjusted amount */ - public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, long maxTradeLimit) { + public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) { return getAdjustedAmount(amount, price, maxTradeLimit, 1); } - public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, long maxTradeLimit) { - DecimalFormat decimalFormat = new DecimalFormat("#.####"); + public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) { + DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS); double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount))); return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount); } @@ -121,7 +121,7 @@ public class CoinUtil { * @return The adjusted amount */ @VisibleForTesting - static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTradeLimit, int factor) { + static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) { checkArgument( amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(), "amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr" @@ -163,11 +163,13 @@ public class CoinUtil { // If we are above our trade limit we reduce the amount by the smallestUnitForAmount BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume); - while (adjustedAmount > maxTradeLimit) { - adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + if (maxTradeLimit != null) { + while (adjustedAmount > maxTradeLimit) { + adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact(); + } } adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, 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..15f6ff8a74 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,18 +162,12 @@ public class Balances { // calculate reserved balance reservedBalance = reservedOfferBalance.add(reservedTradeBalance); + // play sound if funds received + boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; + if (fundsReceived) HavenoUtils.playCashRegisterSound(); + // 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); - }); + updateCounter.set(updateCounter.get() + 1); } } } diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java index 025d6cda69..d863c92e74 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntry.java @@ -122,12 +122,12 @@ public final class XmrAddressEntry implements PersistablePayload { return context == Context.OFFER_FUNDING; } - public boolean isTrade() { + public boolean isTradePayout() { return context == Context.TRADE_PAYOUT; } public boolean isTradable() { - return isOpenOffer() || isTrade(); + return isOpenOffer() || isTradePayout(); } public Coin getCoinLockedInMultiSig() { diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java index 73f7379dfb..984f38bdfc 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java @@ -110,10 +110,25 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted } public void swapToAvailable(XmrAddressEntry addressEntry) { - boolean setChangedByRemove = entrySet.remove(addressEntry); - boolean setChangedByAdd = entrySet.add(new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), - XmrAddressEntry.Context.AVAILABLE)); - if (setChangedByRemove || setChangedByAdd) { + log.info("swapToAvailable addressEntry to swap={}", addressEntry); + if (entrySet.remove(addressEntry)) { + requestPersistence(); + } + // If we have an address entry which shared the address with another one (shared funding use case) + // then we do not swap to available as we need to protect the address of the remaining entry. + boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(entry -> { + if (addressEntry.getAddressString() != null) { + return addressEntry.getAddressString().equals(entry.getAddressString()) && + addressEntry.getContext() == entry.getContext(); + } + return false; + }); + if (entryWithSameContextStillExists) { + return; + } + // no other uses of the address context remain, so make it available + if (entrySet.add(new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), + XmrAddressEntry.Context.AVAILABLE))) { requestPersistence(); } } 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 97217b0fe4..c38ae9b411 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -75,8 +75,6 @@ public class XmrNodes { new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 38081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 39081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "45.63.8.26", 38081, 2, "@haveno"), - new XmrNode(MoneroNodesOption.PROVIDED, null, null, "stagenet.community.rino.io", 38081, 3, "@RINOwallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "stagenet.melo.tools", 38081, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 38089, 3, null) @@ -85,7 +83,6 @@ 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, "node.community.rino.io", 18081, 2, "@RINOwallet"), 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, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), @@ -187,10 +184,6 @@ public class XmrNodes { this.operator = operator; } - public boolean hasOnionAddress() { - return onionAddress != null; - } - public String getHostNameOrAddress() { if (hostName != null) return hostName; @@ -198,10 +191,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/MoneroWalletRpcManager.java b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java index c993ebd181..1bb1e3500e 100644 --- a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java @@ -135,7 +135,7 @@ public class MoneroWalletRpcManager { // stop process String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); - log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid); + log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force); walletRpc.stopProcess(force); } 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 a70d2cc1f3..5cd181a4aa 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -24,11 +24,15 @@ import org.bitcoinj.core.Coin; 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_BUYER_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); - // For the seller we use a fixed one as there is no way the seller can cancel the trade - // To make it editable would just increase complexity. - public static BigInteger MIN_SELLER_SECURITY_DEPOSIT = MIN_BUYER_SECURITY_DEPOSIT; + 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; + // 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. private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE; @@ -53,31 +57,20 @@ public class Restrictions { return MIN_TRADE_AMOUNT; } - public static double getDefaultBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getDefaultSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMinBuyerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. + public static double getMinSecurityDepositAsPercent() { + return MIN_SECURITY_DEPOSIT_PCT; } - public static double getMaxBuyerSecurityDepositAsPercent() { - return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC + public static double getMaxSecurityDepositAsPercent() { + return MAX_SECURITY_DEPOSIT_PCT; } - // We use MIN_BUYER_SECURITY_DEPOSIT as well as lower bound in case of small trade amounts. - // So 0.0005 BTC is the min. buyer security deposit even with amount of 0.0001 BTC and 0.05% percentage value. - public static BigInteger getMinBuyerSecurityDeposit() { - return MIN_BUYER_SECURITY_DEPOSIT; - } - - - public static double getSellerSecurityDepositAsPercent() { - return 0.15; // 15% of trade amount. - } - - public static BigInteger getMinSellerSecurityDeposit() { - return MIN_SELLER_SECURITY_DEPOSIT; + public static BigInteger getMinSecurityDeposit() { + return MIN_SECURITY_DEPOSIT; } // This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT 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 731c1311e0..73332dc4fa 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -36,15 +36,13 @@ import haveno.core.trade.HavenoUtils; /** * Poll for changes to the spent status of key images. - * - * TODO: move to monero-java? */ @Slf4j public class XmrKeyImagePoller { private MoneroDaemon daemon; private long refreshPeriodMs; - private List keyImages = new ArrayList(); + private Map> keyImageGroups = new HashMap>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); @@ -53,9 +51,6 @@ public class XmrKeyImagePoller { /** * Construct the listener. - * - * @param refreshPeriodMs - refresh period in milliseconds - * @param keyImages - key images to listen to */ public XmrKeyImagePoller() { looper = new TaskLooper(() -> poll()); @@ -64,14 +59,13 @@ public class XmrKeyImagePoller { /** * Construct the listener. * + * @param daemon - the Monero daemon to poll * @param refreshPeriodMs - refresh period in milliseconds - * @param keyImages - key images to listen to */ - public XmrKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs, String... keyImages) { + public XmrKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs) { looper = new TaskLooper(() -> poll()); setDaemon(daemon); setRefreshPeriodMs(refreshPeriodMs); - setKeyImages(keyImages); } /** @@ -131,36 +125,13 @@ public class XmrKeyImagePoller { return refreshPeriodMs; } - /** - * Get a copy of the key images being listened to. - * - * @return the key images to listen to - */ - public Collection getKeyImages() { - synchronized (keyImages) { - return new ArrayList(keyImages); - } - } - - /** - * Set the key images to listen to. - * - * @return the key images to listen to - */ - public void setKeyImages(String... keyImages) { - synchronized (this.keyImages) { - this.keyImages.clear(); - addKeyImages(keyImages); - } - } - /** * Add a key image to listen to. * * @param keyImage - the key image to listen to */ - public void addKeyImage(String keyImage) { - addKeyImages(keyImage); + public void addKeyImage(String keyImage, String groupId) { + addKeyImages(Arrays.asList(keyImage), groupId); } /** @@ -168,52 +139,50 @@ public class XmrKeyImagePoller { * * @param keyImages - key images to listen to */ - public void addKeyImages(String... keyImages) { - addKeyImages(Arrays.asList(keyImages)); - } - - /** - * Add key images to listen to. - * - * @param keyImages - key images to listen to - */ - public void addKeyImages(Collection keyImages) { - synchronized (this.keyImages) { - for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage); + public void addKeyImages(Collection keyImages, String groupId) { + synchronized (this.keyImageGroups) { + if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); + Set keyImagesGroup = keyImageGroups.get(groupId); + keyImagesGroup.addAll(keyImages); refreshPolling(); } } - /** - * Remove a key image to listen to. - * - * @param keyImage - the key image to unlisten to - */ - public void removeKeyImage(String keyImage) { - removeKeyImages(keyImage); - } - /** * Remove key images to listen to. * * @param keyImages - key images to unlisten to */ - public void removeKeyImages(String... keyImages) { - removeKeyImages(Arrays.asList(keyImages)); - } - - /** - * Remove key images to listen to. - * - * @param keyImages - key images to unlisten to - */ - public void removeKeyImages(Collection keyImages) { - synchronized (this.keyImages) { - Set containedKeyImages = new HashSet(keyImages); - containedKeyImages.retainAll(this.keyImages); - this.keyImages.removeAll(containedKeyImages); + public void removeKeyImages(Collection keyImages, String groupId) { + synchronized (keyImageGroups) { + Set keyImagesGroup = keyImageGroups.get(groupId); + if (keyImagesGroup == null) return; + keyImagesGroup.removeAll(keyImages); + if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); + Set allKeyImages = getKeyImages(); synchronized (lastStatuses) { - for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); + for (String keyImage : keyImages) { + if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { + lastStatuses.remove(keyImage); + } + } + } + refreshPolling(); + } + } + + public void removeKeyImages(String groupId) { + synchronized (keyImageGroups) { + Set keyImagesGroup = keyImageGroups.get(groupId); + if (keyImagesGroup == null) return; + keyImageGroups.remove(groupId); + Set allKeyImages = getKeyImages(); + synchronized (lastStatuses) { + for (String keyImage : keyImagesGroup) { + if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { + lastStatuses.remove(keyImage); + } + } } refreshPolling(); } @@ -223,7 +192,13 @@ public class XmrKeyImagePoller { * Clear the key images which stops polling. */ public void clearKeyImages() { - setKeyImages(); + synchronized (keyImageGroups) { + keyImageGroups.clear(); + synchronized (lastStatuses) { + lastStatuses.clear(); + } + refreshPolling(); + } } /** @@ -235,10 +210,20 @@ public class XmrKeyImagePoller { public Boolean isSpent(String keyImage) { synchronized (lastStatuses) { if (!lastStatuses.containsKey(keyImage)) return null; - return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT; + return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } } + /** + * Indicates if the given key image spent status is spent. + * + * @param status the key image spent status to check + * @return true if the key image is spent, false if unspent + */ + public static boolean isSpent(MoneroKeyImageSpentStatus status) { + return status != MoneroKeyImageSpentStatus.NOT_SPENT; + } + /** * Get the last known spent status for the given key image. * @@ -257,16 +242,11 @@ public class XmrKeyImagePoller { return; } - // get copy of key images to fetch - List keyImages = new ArrayList(getKeyImages()); - // fetch spent statuses List spentStatuses = null; + List keyImages = new ArrayList(getKeyImages()); try { - if (keyImages.isEmpty()) spentStatuses = new ArrayList(); - else { - spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter - } + spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { // limit error logging @@ -297,8 +277,8 @@ public class XmrKeyImagePoller { } private void refreshPolling() { - synchronized (keyImages) { - setIsPolling(keyImages.size() > 0 && listeners.size() > 0); + synchronized (keyImageGroups) { + setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } @@ -313,4 +293,14 @@ public class XmrKeyImagePoller { looper.stop(); } } + + private Set getKeyImages() { + Set allKeyImages = new HashSet(); + synchronized (keyImageGroups) { + for (Set keyImagesGroup : keyImageGroups.values()) { + allKeyImages.addAll(keyImagesGroup); + } + } + return allKeyImages; + } } 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 81eada3295..594f58b6fb 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -17,6 +17,7 @@ import javafx.beans.property.LongProperty; import javafx.beans.property.SimpleLongProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.common.TaskLooper; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; @@ -24,16 +25,18 @@ import monero.wallet.MoneroWalletFull; import monero.wallet.model.MoneroWalletListener; @Slf4j -public class XmrWalletBase { +public abstract class XmrWalletBase { // constants public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120; public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100; + public static final int SAVE_WALLET_DELAY_SECONDS = 300; // inherited protected MoneroWallet wallet; @Getter protected final Object walletLock = new Object(); + protected Timer saveWalletDelayTimer; @Getter protected XmrConnectionService xmrConnectionService; protected boolean wasWalletSynced; @@ -54,7 +57,7 @@ public class XmrWalletBase { // private private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked - private String testReconnectMonerod1 = "http://node.community.rino.io:18081"; + private String testReconnectMonerod1 = "http://xmr-node.cakewallet.com:18081"; private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081"; public XmrWalletBase() { @@ -102,13 +105,14 @@ public class XmrWalletBase { // 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) { - log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); - if (wallet != null && !isShutDownStarted) log.warn(ExceptionUtils.getStackTrace(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; @@ -137,6 +141,34 @@ public class XmrWalletBase { } } + public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { + if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) { + onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread + return true; + } + return false; + } + + public void saveWalletWithDelay() { + // delay writing to disk to avoid frequent write operations + if (saveWalletDelayTimer == null) { + saveWalletDelayTimer = UserThread.runAfter(() -> { + requestSaveWallet(); + UserThread.execute(() -> saveWalletDelayTimer = null); + }, SAVE_WALLET_DELAY_SECONDS, TimeUnit.SECONDS); + } + } + + // --------------------------------- ABSTRACT ----------------------------- + + public abstract void saveWallet(); + + public abstract void requestSaveWallet(); + + protected abstract void onConnectionChanged(MoneroRpcConnection connection); + + // ------------------------------ PRIVATE HELPERS ------------------------- + private void updateSyncProgress(long height, long targetHeight) { resetSyncProgressTimeout(); UserThread.execute(() -> { 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 1a9ead3597..fc83118cbd 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -68,7 +68,6 @@ import java.util.stream.Stream; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.value.ChangeListener; -import lombok.Getter; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; @@ -111,8 +110,7 @@ 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.ELEVATED; + 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(); private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager(); @@ -121,7 +119,7 @@ public class XmrWalletService extends XmrWalletBase { private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String KEYS_FILE_POSTFIX = ".keys"; private static final String ADDRESS_FILE_POSTFIX = ".address.txt"; - private static final int NUM_MAX_WALLET_BACKUPS = 1; + private static final int NUM_MAX_WALLET_BACKUPS = 2; private static final int MAX_SYNC_ATTEMPTS = 3; private static final boolean PRINT_RPC_STACK_TRACE = false; private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); @@ -145,8 +143,7 @@ public class XmrWalletService extends XmrWalletBase { private TradeManager tradeManager; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type - @Getter - public final Object lock = new Object(); + private final Object lock = new Object(); private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; @@ -245,16 +242,24 @@ public class XmrWalletService extends XmrWalletBase { return user.getWalletCreationDate(); } - public void saveMainWallet() { - saveMainWallet(!(Utilities.isWindows() && wallet != null)); + @Override + public void saveWallet() { + saveWallet(shouldBackup(wallet)); } - public void saveMainWallet(boolean backup) { - saveWallet(getWallet(), backup); + private boolean shouldBackup(MoneroWallet wallet) { + return wallet != null && !Utilities.isWindows(); // TODO: cannot backup on windows because file is locked } - public void requestSaveMainWallet() { - ThreadUtils.submitToPool(() -> saveMainWallet()); // save wallet off main thread + public void saveWallet(boolean backup) { + synchronized (walletLock) { + saveWallet(getWallet(), backup); + } + } + + @Override + public void requestSaveWallet() { + ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread } public boolean isWalletAvailable() { @@ -376,8 +381,8 @@ public class XmrWalletService extends XmrWalletBase { } public void saveWallet(MoneroWallet wallet, boolean backup) { - wallet.save(); if (backup) backupWallet(getWalletName(wallet.getPath())); + wallet.save(); } public void closeWallet(MoneroWallet wallet, boolean save) { @@ -385,8 +390,8 @@ public class XmrWalletService extends XmrWalletBase { MoneroError err = null; String path = wallet.getPath(); try { - wallet.close(save); - if (save) backupWallet(getWalletName(path)); + if (save) saveWallet(wallet, shouldBackup(wallet)); + wallet.close(); } catch (MoneroError e) { err = e; } @@ -411,7 +416,7 @@ public class XmrWalletService extends XmrWalletBase { public void deleteWallet(String walletName) { assertNotPath(walletName); log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); - if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); + if (!walletExists(walletName)) throw new RuntimeException("Wallet does not exist at path: " + walletName); String path = walletDir.toString() + File.separator + walletName; if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet cache file: " + path); if (!new File(path + KEYS_FILE_POSTFIX).delete()) throw new RuntimeException("Failed to delete wallet keys file: " + path + KEYS_FILE_POSTFIX); @@ -443,7 +448,7 @@ public class XmrWalletService extends XmrWalletBase { if (Boolean.TRUE.equals(txConfig.getRelay())) { cachedTxs.addFirst(tx); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } return tx; } @@ -453,7 +458,7 @@ public class XmrWalletService extends XmrWalletBase { public String relayTx(String metadata) { synchronized (walletLock) { String txId = wallet.relayTx(metadata); - requestSaveMainWallet(); + requestSaveWallet(); return txId; } } @@ -552,7 +557,7 @@ public class XmrWalletService extends XmrWalletBase { // freeze outputs for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } } @@ -574,19 +579,10 @@ public class XmrWalletService extends XmrWalletBase { // thaw outputs for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage); cacheWalletInfo(); - requestSaveMainWallet(); + requestSaveWallet(); } } - public BigInteger getOutputsAmount(Collection keyImages) { - BigInteger sum = BigInteger.ZERO; - for (String keyImage : keyImages) { - List outputs = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); - if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount()); - } - return sum; - } - private List getSubaddressesWithExactInput(BigInteger amount) { // fetch unspent, unfrozen, unlocked outputs @@ -692,22 +688,24 @@ public class XmrWalletService extends XmrWalletBase { } private MoneroTxWallet createTradeTxFromSubaddress(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, Integer subaddressIndex) { + synchronized (walletLock) { - // create tx - MoneroTxConfig txConfig = new MoneroTxConfig() - .setAccountIndex(0) - .setSubaddressIndices(subaddressIndex) - .addDestination(sendAddress, sendAmount) - .setSubtractFeeFrom(0) // pay mining fee from send amount - .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); - if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(feeAddress, feeAmount); - MoneroTxWallet tradeTx = createTx(txConfig); + // create tx + MoneroTxConfig txConfig = new MoneroTxConfig() + .setAccountIndex(0) + .setSubaddressIndices(subaddressIndex) + .addDestination(sendAddress, sendAmount) + .setSubtractFeeFrom(0) // pay mining fee from send amount + .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); + if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(feeAddress, feeAmount); + MoneroTxWallet tradeTx = createTx(txConfig); - // freeze inputs - List keyImages = new ArrayList(); - for (MoneroOutput input : tradeTx.getInputs()) keyImages.add(input.getKeyImage().getHex()); - freezeOutputs(keyImages); - return tradeTx; + // freeze inputs + List keyImages = new ArrayList(); + for (MoneroOutput input : tradeTx.getInputs()) keyImages.add(input.getKeyImage().getHex()); + freezeOutputs(keyImages); + return tradeTx; + } } public MoneroTx verifyReserveTx(String offerId, BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, String txHash, String txHex, String txKey, List keyImages) { @@ -741,7 +739,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; - synchronized (daemon) { + synchronized (lock) { try { // verify tx not submitted to pool @@ -760,17 +758,16 @@ public class XmrWalletService extends XmrWalletBase { if (keyImages != null) { Set txKeyImages = new HashSet(); for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); - if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Tx inputs do not match claimed key images"); + if (!txKeyImages.equals(new HashSet(keyImages))) throw new RuntimeException("Tx inputs do not match claimed key images"); } // verify unlock height if (!BigInteger.ZERO.equals(tx.getUnlockTime())) throw new RuntimeException("Unlock height must be 0"); // verify miner fee - BigInteger minerFeeEstimate = getElevatedFeeEstimate(tx.getWeight()); - double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); - if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee()); - log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); + BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); + HavenoUtils.verifyMinerFee(minerFeeEstimate, tx.getFee()); + log.info("Trade miner fee {} is within tolerance"); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; @@ -786,11 +783,23 @@ public class XmrWalletService extends XmrWalletBase { BigInteger actualSendAmount = transferCheck.getReceivedAmount(); // verify trade fee amount - if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); + if (!actualTradeFee.equals(tradeFeeAmount)) { + if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) { + log.warn("Trade fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); + } else { + throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); + } + } // verify send amount BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee()); - if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + if (!actualSendAmount.equals(expectedSendAmount)) { + if (equalsWithinFractionError(actualSendAmount, expectedSendAmount)) { + log.warn("Trade tx send amount is within fraction error, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + } else { + throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); + } + } return tx; } catch (Exception e) { log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage()); @@ -806,17 +815,30 @@ public class XmrWalletService extends XmrWalletBase { } } + // TODO: old bug in atomic unit conversion could cause fractional difference error, remove this in future release, maybe re-sign all offers then + private static boolean equalsWithinFractionError(BigInteger a, BigInteger b) { + return a.subtract(b).abs().compareTo(new BigInteger("1")) <= 0; + } + /** * Get the tx fee estimate based on its weight. * * @param txWeight - the tx weight * @return the tx fee estimate */ - private BigInteger getElevatedFeeEstimate(long txWeight) { + private BigInteger getFeeEstimate(long txWeight) { + + // get fee priority + MoneroTxPriority priority; + if (PROTOCOL_FEE_PRIORITY == MoneroTxPriority.DEFAULT) { + priority = wallet.getDefaultFeePriority(); + } else { + priority = PROTOCOL_FEE_PRIORITY; + } // get fee estimates per kB from daemon MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); - BigInteger baseFeeEstimate = feeEstimates.getFees().get(2); // get elevated fee per kB + BigInteger baseFeeEstimate = feeEstimates.getFees().get(priority.ordinal() - 1); BigInteger qmask = feeEstimates.getQuantizationMask(); log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask); @@ -902,7 +924,7 @@ public class XmrWalletService extends XmrWalletBase { } // shut down threads - synchronized (getLock()) { + synchronized (lock) { List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID)); ThreadUtils.awaitTasks(shutDownThreads); @@ -986,14 +1008,30 @@ public class XmrWalletService extends XmrWalletBase { public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) { Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); addressEntryOptional.ifPresent(e -> { - log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); xmrAddressEntryList.swapToAvailable(e); saveAddressEntryList(); }); } + public synchronized void cloneAddressEntries(String offerId, String cloneOfferId) { + List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).collect(Collectors.toList()); + for (XmrAddressEntry entry : entries) { + XmrAddressEntry clonedEntry = new XmrAddressEntry(entry.getSubaddressIndex(), entry.getAddressString(), entry.getContext(), cloneOfferId, null); + Optional existingEntry = getAddressEntry(clonedEntry.getOfferId(), clonedEntry.getContext()); + if (existingEntry.isPresent()) continue; + xmrAddressEntryList.addAddressEntry(clonedEntry); + } + } + public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + + // skip if failed trade is scheduled for processing // TODO: do not call this function in this case? + if (tradeManager.hasFailedScheduledTrade(offerId)) { + log.warn("Refusing to reset address entries because trade is scheduled for deletion with offerId={}", offerId); + return; + } + swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); // swap trade payout to available if applicable @@ -1002,7 +1040,7 @@ public class XmrWalletService extends XmrWalletBase { if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } - public synchronized void resetAddressEntriesForTrade(String offerId) { + public synchronized void swapPayoutAddressEntryToAvailable(String offerId) { swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } @@ -1125,6 +1163,15 @@ public class XmrWalletService extends XmrWalletBase { return subaddress == null ? BigInteger.ZERO : subaddress.getBalance(); } + public BigInteger getBalanceForSubaddress(int subaddressIndex, boolean includeFrozen) { + return getBalanceForSubaddress(subaddressIndex).add(includeFrozen ? getFrozenBalanceForSubaddress(subaddressIndex) : BigInteger.ZERO); + } + + public BigInteger getFrozenBalanceForSubaddress(int subaddressIndex) { + List outputs = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setAccountIndex(0).setSubaddressIndex(subaddressIndex)); + return outputs.stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); + } + public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { MoneroSubaddress subaddress = getSubaddress(subaddressIndex); return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance(); @@ -1133,7 +1180,7 @@ public class XmrWalletService extends XmrWalletBase { public Stream getAddressEntriesForAvailableBalanceStream() { 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().getOpenOfferById(entry.getOfferId()).isPresent())); + 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())); return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } @@ -1153,26 +1200,34 @@ public class XmrWalletService extends XmrWalletBase { // TODO (woodser): update balance and other listening public void addBalanceListener(XmrBalanceListener listener) { - if (!balanceListeners.contains(listener)) balanceListeners.add(listener); + if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); + synchronized (balanceListeners) { + if (!balanceListeners.contains(listener)) balanceListeners.add(listener); + } } public void removeBalanceListener(XmrBalanceListener listener) { - balanceListeners.remove(listener); + if (listener == null) throw new IllegalArgumentException("Cannot add null balance listener"); + synchronized (balanceListeners) { + balanceListeners.remove(listener); + } } public void updateBalanceListeners() { BigInteger availableBalance = getAvailableBalance(); - for (XmrBalanceListener balanceListener : balanceListeners) { - BigInteger balance; - if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); - else balance = availableBalance; - ThreadUtils.submitToPool(() -> { - try { - balanceListener.onBalanceChanged(balance); - } catch (Exception e) { - log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); - } - }); + synchronized (balanceListeners) { + for (XmrBalanceListener balanceListener : balanceListeners) { + BigInteger balance; + if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); + else balance = availableBalance; + ThreadUtils.submitToPool(() -> { + try { + balanceListener.onBalanceChanged(balance); + } catch (Exception e) { + log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); + } + }); + } } } @@ -1219,10 +1274,29 @@ public class XmrWalletService extends XmrWalletBase { return cachedAvailableBalance; } + public boolean hasAddress(String address) { + for (MoneroSubaddress subaddress : getSubaddresses()) { + if (subaddress.getAddress().equals(address)) return true; + } + return false; + } + public List getSubaddresses() { return cachedSubaddresses; } + public BigInteger getAmountSentToSelf(MoneroTxWallet tx) { + BigInteger sentToSelfAmount = BigInteger.ZERO; + if (tx.getOutgoingTransfer() != null && tx.getOutgoingTransfer().getDestinations() != null) { + for (MoneroDestination destination : tx.getOutgoingTransfer().getDestinations()) { + if (hasAddress(destination.getAddress())) { + sentToSelfAmount = sentToSelfAmount.add(destination.getAmount()); + } + } + } + return sentToSelfAmount; + } + public List getOutputs(MoneroOutputQuery query) { List filteredOutputs = new ArrayList(); for (MoneroOutputWallet output : cachedOutputs) { @@ -1231,6 +1305,19 @@ public class XmrWalletService extends XmrWalletBase { return filteredOutputs; } + public List getOutputs(Collection keyImages) { + List outputs = new ArrayList(); + for (String keyImage : keyImages) { + List outputList = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); + if (!outputList.isEmpty()) outputs.add(outputList.get(0)); + } + return outputs; + } + + public BigInteger getOutputsAmount(Collection keyImages) { + return getOutputs(keyImages).stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Util /////////////////////////////////////////////////////////////////////////////////////////// @@ -1278,10 +1365,16 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } else { + // 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(); - return; } }); @@ -1301,11 +1394,12 @@ public class XmrWalletService extends XmrWalletBase { maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } - private void maybeInitMainWallet(boolean sync, int numAttempts) { + private void maybeInitMainWallet(boolean sync, int numSyncAttempts) { ThreadUtils.execute(() -> { try { doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } catch (Exception e) { + if (isShutDownStarted) return; log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); HavenoUtils.setTopError(e.getMessage()); throw e; @@ -1313,7 +1407,7 @@ public class XmrWalletService extends XmrWalletBase { }, THREAD_ID); } - private void doMaybeInitMainWallet(boolean sync, int numAttempts) { + private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) { synchronized (walletLock) { if (isShutDownStarted) return; @@ -1341,7 +1435,7 @@ public class XmrWalletService extends XmrWalletBase { // sync main wallet if applicable // TODO: error handling and re-initialization is jenky, refactor - if (sync && numAttempts > 0) { + if (sync && numSyncAttempts > 0) { try { // switch connection if disconnected @@ -1357,10 +1451,10 @@ public class XmrWalletService extends XmrWalletBase { 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, numAttempts - 1); // re-initialize wallet and sync again + maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again return; } log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); @@ -1376,8 +1470,8 @@ public class XmrWalletService extends XmrWalletBase { log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); } - // reapply connection after wallet synced (might reinitialize wallet on new thread) - ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); + // reapply connection after wallet synced (might reinitialize wallet with proxy) + onConnectionChanged(xmrConnectionService.getConnection()); // reset internal state if main wallet was swapped resetIfWalletChanged(); @@ -1391,14 +1485,14 @@ public class XmrWalletService extends XmrWalletBase { HavenoUtils.havenoSetup.getWalletInitialized().set(true); // save but skip backup on initialization - saveMainWallet(false); + 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 (numAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); + if (numSyncAttempts <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts); HavenoUtils.havenoSetup.getWalletInitialized().set(true); - saveMainWallet(false); + saveWallet(false); // reschedule to init main wallet UserThread.runAfter(() -> { @@ -1407,7 +1501,7 @@ public class XmrWalletService extends XmrWalletBase { } else { log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> { - maybeInitMainWallet(true, numAttempts - 1); + maybeInitMainWallet(true, numSyncAttempts - 1); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } @@ -1473,30 +1567,38 @@ public class XmrWalletService extends XmrWalletBase { // try opening wallet config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); - log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); + log.info("Opening full wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); try { walletFull = MoneroWalletFull.openWallet(config); } catch (Exception e) { - log.warn("Failed to open full wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage()); + if (isShutDownStarted) throw e; + log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { // rename wallet cache to backup - String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME; + String cachePath = walletDir.toString() + File.separator + getWalletName(config.getPath()); File originalCacheFile = new File(cachePath); if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup")); - // copy latest wallet cache backup to main folder - File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME); - if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath)); + // try opening wallet with backup cache files in descending order + List backupCacheFiles = FileUtil.getBackupFiles(walletDir, getWalletName(config.getPath())); + Collections.reverse(backupCacheFiles); + for (File backupCacheFile : backupCacheFiles) { + try { + FileUtil.copyFile(backupCacheFile, new File(cachePath)); + walletFull = MoneroWalletFull.openWallet(config); + log.warn("Successfully opened full wallet using backup cache"); + retrySuccessful = true; + break; + } catch (Exception e2) { - // retry opening wallet without original cache - try { - walletFull = MoneroWalletFull.openWallet(config); - log.info("Successfully opened full wallet using backup cache"); - retrySuccessful = true; - } catch (Exception e2) { - // ignore + // delete cache file if failed to open + File cacheFile = new File(cachePath); + if (cacheFile.exists()) cacheFile.delete(); + File unportableCacheFile = new File(cachePath + ".unportable"); + if (unportableCacheFile.exists()) unportableCacheFile.delete(); + } } // handle success or failure @@ -1505,14 +1607,30 @@ public class XmrWalletService extends XmrWalletBase { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { - // restore original wallet cache - log.warn("Failed to open full wallet using backup cache, restoring original cache"); - File cacheFile = new File(cachePath); - if (cacheFile.exists()) cacheFile.delete(); - if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); + // retry opening wallet after cache deleted + try { + log.warn("Failed to open full wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); + walletFull = MoneroWalletFull.openWallet(config); + log.warn("Successfully opened full wallet after cache deleted"); + retrySuccessful = true; + } catch (Exception e2) { + // ignore + } - // throw exception - throw e; + // handle success or failure + if (retrySuccessful) { + if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup + } else { + + // restore original wallet cache + log.warn("Failed to open full wallet '{}' after deleting cache, restoring original cache", config.getPath()); + File cacheFile = new File(cachePath); + if (cacheFile.exists()) cacheFile.delete(); + if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); + + // throw original exception + throw e; + } } } catch (Exception e2) { throw e; // throw original exception @@ -1546,6 +1664,7 @@ public class XmrWalletService extends XmrWalletBase { walletRpc.stopSyncing(); // create wallet + if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet '" + config.getPath() + "' because shutdown is started"); MoneroRpcConnection connection = xmrConnectionService.getConnection(); log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); long time = System.currentTimeMillis(); @@ -1555,9 +1674,8 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { - log.warn("Could not create wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); - throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno."); + throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage()); } } @@ -1577,31 +1695,40 @@ public class XmrWalletService extends XmrWalletBase { if (!applyProxyUri) connection.setProxyUri(null); // try opening wallet - log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); + if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet '" + config.getPath() + "' because shutdown is started"); + log.info("Opening RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri()); config.setServer(connection); try { walletRpc.openWallet(config); } catch (Exception e) { - log.warn("Failed to open RPC wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage()); + if (isShutDownStarted) throw e; + log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage()); boolean retrySuccessful = false; try { // rename wallet cache to backup - String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME; + String cachePath = walletDir.toString() + File.separator + config.getPath(); File originalCacheFile = new File(cachePath); if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup")); - // copy latest wallet cache backup to main folder - File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME); - if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath)); + // try opening wallet with backup cache files in descending order + List backupCacheFiles = FileUtil.getBackupFiles(walletDir, config.getPath()); + Collections.reverse(backupCacheFiles); + for (File backupCacheFile : backupCacheFiles) { + try { + FileUtil.copyFile(backupCacheFile, new File(cachePath)); + walletRpc.openWallet(config); + log.warn("Successfully opened RPC wallet using backup cache"); + retrySuccessful = true; + break; + } catch (Exception e2) { - // retry opening wallet without original cache - try { - walletRpc.openWallet(config); - log.info("Successfully opened RPC wallet using backup cache"); - retrySuccessful = true; - } catch (Exception e2) { - // ignore + // delete cache file if failed to open + File cacheFile = new File(cachePath); + if (cacheFile.exists()) cacheFile.delete(); + File unportableCacheFile = new File(cachePath + ".unportable"); + if (unportableCacheFile.exists()) unportableCacheFile.delete(); + } } // handle success or failure @@ -1610,14 +1737,30 @@ public class XmrWalletService extends XmrWalletBase { if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { - // restore original wallet cache - log.warn("Failed to open RPC wallet using backup cache, restoring original cache"); - File cacheFile = new File(cachePath); - if (cacheFile.exists()) cacheFile.delete(); - if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); + // retry opening wallet after cache deleted + try { + log.warn("Failed to open RPC wallet '{}' using backup cache files, retrying with cache deleted", config.getPath()); + walletRpc.openWallet(config); + log.warn("Successfully opened RPC wallet after cache deleted"); + retrySuccessful = true; + } catch (Exception e2) { + // ignore + } - // throw exception - throw e; + // handle success or failure + if (retrySuccessful) { + if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup + } else { + + // restore original wallet cache + log.warn("Failed to open RPC wallet '{}' after deleting cache, restoring original cache", config.getPath()); + File cacheFile = new File(cachePath); + if (cacheFile.exists()) cacheFile.delete(); + if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); + + // throw original exception + throw e; + } } } catch (Exception e2) { throw e; // throw original exception @@ -1627,7 +1770,6 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done opening RPC wallet " + config.getPath()); return walletRpc; } catch (Exception e) { - log.warn("Could not open wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); throw new IllegalStateException("Could not open wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage()); } @@ -1636,7 +1778,7 @@ public class XmrWalletService extends XmrWalletBase { private MoneroWalletRpc startWalletRpcInstance(Integer port, boolean applyProxyUri) { // check if monero-wallet-rpc exists - if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new RuntimeException("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system"); // build command to start monero-wallet-rpc @@ -1675,7 +1817,8 @@ public class XmrWalletService extends XmrWalletBase { return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } - private void onConnectionChanged(MoneroRpcConnection connection) { + @Override + protected void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { // use current connection @@ -1729,7 +1872,7 @@ public class XmrWalletService extends XmrWalletBase { tasks.add(() -> { try { wallet.changePassword(oldPassword, newPassword); - saveMainWallet(); + saveWallet(); } catch (Exception e) { log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e); throw e; @@ -1779,13 +1922,13 @@ public class XmrWalletService extends XmrWalletBase { log.warn("Force restarting main wallet"); if (isClosingWallet) return; forceCloseMainWallet(); - maybeInitMainWallet(true); + doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while - if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); - getWallet(); // re-open wallet + requestSwitchToNextBestConnection(sourceConnection); + if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } private void startPolling() { @@ -1837,10 +1980,18 @@ public class XmrWalletService extends XmrWalletBase { } private void doPollWallet(boolean updateTxs) { + + // skip if shut down started + if (isShutDownStarted) return; + + // set poll in progress + boolean pollInProgressSet = false; synchronized (pollLock) { + if (!pollInProgress) pollInProgressSet = true; pollInProgress = true; } - if (isShutDownStarted) return; + + // poll wallet try { // skip if daemon not synced @@ -1903,8 +2054,10 @@ public class XmrWalletService extends XmrWalletBase { //e.printStackTrace(); } } finally { - synchronized (pollLock) { - pollInProgress = false; + if (pollInProgressSet) { + synchronized (pollLock) { + pollInProgress = false; + } } // cache wallet info last @@ -1912,7 +2065,7 @@ public class XmrWalletService extends XmrWalletBase { if (wallet != null && !isShutDownStarted) { try { cacheWalletInfo(); - requestSaveMainWallet(); + saveWalletWithDelay(); } catch (Exception e) { log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } @@ -1944,10 +2097,6 @@ public class XmrWalletService extends XmrWalletBase { return requestSwitchToNextBestConnection(null); } - public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { - return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection); - } - private void onNewBlock(long height) { UserThread.execute(() -> { walletHeight.set(height); diff --git a/core/src/main/resources/bip39_english.txt b/core/src/main/resources/bip39_english.txt new file mode 100644 index 0000000000..942040ed50 --- /dev/null +++ b/core/src/main/resources/bip39_english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 45e071114a..9d500aefe5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -43,6 +43,8 @@ shared.buyMonero=Buy Monero shared.sellMonero=Sell Monero shared.buyCurrency=Buy {0} shared.sellCurrency=Sell {0} +shared.buyCurrency.locked=Buy {0} 🔒 +shared.sellCurrency.locked=Sell {0} 🔒 shared.buyingXMRWith=buying XMR with {0} shared.sellingXMRFor=selling XMR for {0} shared.buyingCurrency=buying {0} (selling XMR) @@ -101,13 +103,12 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer -shared.duplicateOffer=Duplicate offer shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page shared.yesCancel=Yes, cancel shared.nextStep=Next step -shared.fundFromSavingsWalletButton=Transfer funds from Haveno wallet +shared.fundFromSavingsWalletButton=Apply funds from Haveno wallet shared.fundFromExternalWalletButton=Open your external wallet for funding shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Below % from market price @@ -132,7 +133,7 @@ shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date -shared.sendFundsDetailsWithFee=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2}\n\nThe recipient will receive: {3}\n\nAre you sure you want to withdraw this amount? +shared.sendFundsDetailsWithFee=Sending: {0}\n\nTo receiving address: {1}\n\nAdditional miner fee: {2}\n\nAre you sure you want to send this amount? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard @@ -150,6 +151,7 @@ shared.addNewAccount=Add new account shared.ExportAccounts=Export Accounts shared.importAccounts=Import Accounts shared.createNewAccount=Create new account +shared.createNewAccountDescription=Your account details are stored locally on your device and shared only with your trading peer and the arbitrator if a dispute is opened. shared.saveNewAccount=Save new account shared.selectedAccount=Selected account shared.deleteAccount=Delete account @@ -197,7 +199,7 @@ shared.total=Total shared.totalsNeeded=Funds needed shared.tradeWalletAddress=Trade wallet address shared.tradeWalletBalance=Trade wallet balance -shared.reserveExactAmount=Reserve only the funds needed. May require a mining fee and 10 confirmations (~20 minutes) before your offer is live. +shared.reserveExactAmount=Reserve only the necessary funds. Requires a mining fee and ~20 minutes before your offer goes live. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=I confirm @@ -207,6 +209,7 @@ shared.crypto=Crypto shared.traditional=Traditional shared.otherAssets=other assets shared.other=Other +shared.preciousMetals=Precious Metals shared.all=All shared.edit=Edit shared.advancedOptions=Advanced options @@ -234,6 +237,7 @@ shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker +shared.none=None #################################################################### @@ -245,8 +249,8 @@ shared.taker=Taker #################################################################### mainView.menu.market=Market -mainView.menu.buy=Buy -mainView.menu.sell=Sell +mainView.menu.buyXmr=Buy XMR +mainView.menu.sellXmr=Sell XMR mainView.menu.portfolio=Portfolio mainView.menu.funds=Funds mainView.menu.support=Support @@ -347,6 +351,7 @@ market.trades.showVolumeInUSD=Show volume in USD offerbook.createOffer=Create offer 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} @@ -358,6 +363,8 @@ offerbook.availableOffersToSell=Sell {0} for {1} offerbook.filterByCurrency=Choose currency offerbook.filterByPaymentMethod=Choose payment method offerbook.matchingOffers=Offers matching my accounts +offerbook.filterNoDeposit=No deposit +offerbook.noDepositOffers=Offers with no deposit (passphrase required) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted @@ -375,6 +382,23 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=buy XMR from a signed accoun offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.buyXmrWith=Buy XMR with: +offerbook.sellXmrFor=Sell XMR for: + +offerbook.cloneOffer=Clone offer with shared funds +offerbook.clonedOffer.tooltip=This is a cloned offer with shared funds.\n\Group ID: {0} +offerbook.nonClonedOffer.tooltip=Regular offer without shared funds.\n\Maker reserve transaction ID: {0} +offerbook.hasConflictingClone.warning=This cloned offer with shared funds cannot be activated because it uses \ + the same payment method and currency as another active offer.\n\n\ + You need to edit the offer and change the \ + payment method or currency or deactivate the offer which has the same payment method and currency. +offerbook.cannotActivateEditedOffer.warning=You can't activate an offer that is currently edited. +offerbook.clonedOffer.headline=Cloning an offer +offerbook.clonedOffer.info=Cloning an offer creates a copy without reserving additional funds.\n\n\ + This helps reduce locked capital, making it easier to list the same offer across multiple markets or payment methods.\n\n\ + If one of the cloned offers is taken, the others will close automatically, since they all share the same reserved funds.\n\n\ + Cloned offers must share the same trade amount and security deposit, but use a different payment method or currency.\n\n\ + 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. @@ -389,7 +413,7 @@ offerbook.volume={0} (min - max) 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.createNewOffer=Create new offer to {0} {1} +offerbook.createNewOffer=Create offer to {0} {1} offerbook.createOfferDisabled.tooltip=You can only create one offer at a time offerbook.takeOfferButton.tooltip=Take offer for {0} @@ -410,7 +434,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\n\ After 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\n\ - For more information on account signing, please see the documentation at [HYPERLINK:https://haveno.exchange/wiki/Account_limits#Account_signing]. + 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\ @@ -434,8 +458,9 @@ 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.signatureNotValidated=This offer cannot be taken because the arbitrator's signature is invalid +offerbook.warning.arbitratorNotValidated=This offer cannot be taken because the arbitrator is invalid. +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. offerbook.info.sellAtMarketPrice=You will sell at market price (updated every minute). offerbook.info.buyAtMarketPrice=You will buy at market price (updated every minute). @@ -486,6 +511,7 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} 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 @@ -523,7 +549,10 @@ 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.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Minimum security deposit is used +createOffer.buyerAsTakerWithoutDeposit=No deposit required from buyer (passphrase protected) +createOffer.myDeposit=My security deposit (%) +createOffer.myDepositInfo=Your security deposit will be {0} #################################################################### @@ -549,6 +578,8 @@ takeOffer.fundsBox.networkFee=Total mining fees takeOffer.fundsBox.takeOfferSpinnerInfo=Taking offer: {0} takeOffer.fundsBox.paymentLabel=Haveno trade with ID {0} takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee) +takeOffer.fundsBox.noFundingRequiredTitle=No funding required +takeOffer.fundsBox.noFundingRequiredDescription=Get the offer passphrase from the seller outside Haveno to take this offer. takeOffer.success.headline=You have successfully taken an offer. takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open trades\". takeOffer.error.message=An error occurred when taking the offer.\n\n{0} @@ -585,6 +616,7 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined #################################################################### openOffer.header.triggerPrice=Trigger price +openOffer.header.groupId=Group ID openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\ Please edit the offer to define a new trigger price @@ -595,6 +627,21 @@ editOffer.publishOffer=Publishing your offer. editOffer.failed=Editing of offer failed:\n{0} editOffer.success=Your offer has been successfully edited. editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by Haveno and can no longer be edited. +editOffer.openTabWarning=You have already the \"Edit Offer\" tab open. +editOffer.hasConflictingClone=You have edited an offer which uses shared funding with another offer and your edit \ + made the payment method and currency now the same as another active cloned offer. Your edited offer will be \ + deactivated because it is not permitted to publish 2 offers sharing the funds with the same payment method \ + and currency.\n\n\ + You can edit the offer again at \"Portfolio/My open offers\" to fulfill the requirements to activate it. + +cloneOffer.clone=Clone offer +cloneOffer.publishOffer=Publishing cloned offer. +cloneOffer.success=Your offer has been successfully cloned. +cloneOffer.hasConflictingClone=You have not changed the payment method or the currency. You still can clone the offer, but it will \ + be deactivated and not published.\n\n\ + You can edit the offer later again at \"Portfolio/My open offers\" to fulfill the requirements to activate it.\n\n\ + Do you still want to clone the offer? +cloneOffer.openTabWarning=You have already the \"Clone Offer\" tab open. #################################################################### # Portfolio @@ -605,7 +652,8 @@ portfolio.tab.pendingTrades=Open trades portfolio.tab.history=History portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer -portfolio.tab.duplicateOffer=Duplicate offer +portfolio.tab.duplicateOffer=Create offer +portfolio.tab.cloneOpenOffer=Clone offer portfolio.context.offerLikeThis=Create new offer like this... portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. @@ -658,7 +706,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Deposit transaction has been published.\n{0} need to wait for 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.you=Deposit transaction has been published.\nYou need to wait for 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.buyer=Deposit transaction has been published.\nThe XMR buyer needs to wait for 10 confirmations (about 20 minutes) before the payment can start. portfolio.pending.step1.warn=The deposit transaction is not confirmed yet. This usually takes about 20 minutes, but could be more if the network is congested. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. \ If you have been waiting for much longer than 20 minutes, contact Haveno support. @@ -673,7 +722,7 @@ portfolio.pending.step2_buyer.refTextWarn=Important: when making the payment, le portfolio.pending.step2_buyer.fees=If your bank charges you any fees to make the transfer, you are responsible for paying those fees. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=Make sure to use the SHA (shared fee model) to send the SWIFT payment. \ - See more details at [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option]. + See more details at [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option]. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Please transfer from your external {0} wallet\n{1} to the XMR seller.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -693,7 +742,7 @@ portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\ # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n + 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 # suppress inspection "TrailingSpacesInProperty" @@ -948,7 +997,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee trans 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/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Feel free to move this trade to failed trades. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ @@ -958,7 +1007,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=T (with seller receiving full trade amount back as well). \ This way, there is no security risk, and only trade fees are lost. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [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\n\ If the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open \ @@ -967,18 +1016,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= their security deposits (with seller receiving full trade amount back as well). \ Otherwise the trade amount should go to the buyer. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\n\ Error: {0}\n\n\ It 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\n\ If 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/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\n\ The trade cannot be completed and you might \ have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\n\ Do you want to move the trade to failed trades?\n\n\ @@ -1311,6 +1360,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=My onion address settings.net.xmrNodesLabel=Use custom Monero nodes settings.net.moneroPeersLabel=Connected peers +settings.net.connection=Connection +settings.net.connected=Connected settings.net.useTorForXmrJLabel=Use Tor for Monero network settings.net.useTorForXmrAfterSyncRadio=After wallet is synchronized settings.net.useTorForXmrOffRadio=Never @@ -1962,6 +2013,7 @@ offerDetailsWindow.confirm.taker=Confirm: Take offer to {0} monero offerDetailsWindow.confirm.takerCrypto=Confirm: Take offer to {0} {1} offerDetailsWindow.creationDate=Creation date offerDetailsWindow.makersOnion=Maker's onion address +offerDetailsWindow.challenge=Offer passphrase qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -2010,7 +2062,7 @@ tacWindow.disagree=I disagree and quit tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=Trade -tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID: +tradeDetailsWindow.disputedPayoutTxId=Disputed payout transaction ID tradeDetailsWindow.tradeDate=Trade date tradeDetailsWindow.txFee=Mining fee tradeDetailsWindow.tradePeersOnion=Trading peers onion address @@ -2038,6 +2090,14 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Enter password to unlock +xmrConnectionError.headline=Monero connection error +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 +xmrConnectionError.localNode.fallback=Use next best node + torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges torNetworkSettingWindow.providedBridges=Connect with provided bridges @@ -2260,6 +2320,12 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=No deposit required from buyer +popup.info.buyerAsTakerWithoutDeposit=\ + Your offer will not require a security deposit or fee from the XMR buyer.\n\n\ + To accept your offer, you must share a passphrase with your trade partner outside Haveno.\n\n\ + The passphrase is automatically generated and shown in the offer details after creation.\ + popup.info.torMigration.msg=Your Haveno node is probably using a deprecated Tor v2 address. \ Please switch your Haveno node to a Tor v3 address. \ Make sure to back up your data directory beforehand. @@ -2399,11 +2465,14 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} amount{1} formatter.makerTaker=Maker as {0} {1} / Taker as {2} {3} +formatter.makerTaker.locked=Maker as {0} {1} / Taker as {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=You are {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=You are creating an offer to {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=You are creating an offer to {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=You are creating an offer to {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=You are creating an offer to {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} as maker formatter.asTaker={0} {1} as taker @@ -2645,7 +2714,7 @@ payment.limits.info=Please be aware that all bank transfers carry a certain amou \n\ This limit only applies to the size of a single trade—you can place as many trades as you like.\n\ \n\ - See more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based \ on the following 2 factors:\n\n\ @@ -2663,7 +2732,7 @@ payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade \n\ These limits only apply to the size of a single trade—you can place as many trades as you like. \n\ \n\ - See more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + See more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \ For example, Bank of America and Wells Fargo no longer allow such deposits. @@ -2675,9 +2744,9 @@ payment.account.revolut.addUserNameInfo={0}\n\ This will not affect your account age signing status. payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.cashapp.info=Cash App has higher chargeback risk than most bank transfers. Please be aware of this when trading with Cash App. -payment.venmo.info=Venmo has higher chargeback risk than most bank transfers. Please be aware of this when trading with Venmo. -payment.paypal.info=PayPal has higher chargeback risk than most bank transfers. Please be aware of this when trading with PayPal. +payment.cashapp.info=Please be aware that Cash App has higher chargeback risk than most bank transfers. +payment.venmo.info=Please be aware that Venmo has higher chargeback risk than most bank transfers. +payment.paypal.info=Please be aware that PayPal has higher chargeback risk than most bank transfers. payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\n\ @@ -2693,20 +2762,20 @@ payment.swift.info.account=Carefully review the core guidelines for using SWIFT - buyer must use the shared fee model (SHA) \n\ - buyer and seller may incur fees, so they should check their bank's fee schedules beforehand \n\ \n\ -SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +SWIFT is more sophisticated than other payment methods, so please take a moment to review full guidance on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.buyer=To buy monero with SWIFT, you must:\n\ \n\ - send payment in the currency specified by the offer maker \n\ - use the shared fee model (SHA) to send payment\n\ \n\ -Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT senders are required to use the shared fee model (SHA) to send payments.\n\ \n\ If you receive a SWIFT payment that does not use SHA, open a mediation ticket.\n\ \n\ -Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Please review further guidance on the wiki to avoid penalties and ensure smooth trades [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.imps.info.account=Please make sure to include your:\n\n\ ● Account owner full name\n\ @@ -2938,14 +3007,14 @@ The maximum trade size is $1,000 per payment.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. payment.transferwiseUsd.info.account=Due to US banking regulation, sending and receiving USD payments has more restrictions \ - than most other currencies. For this reason USD was not added to Haveno TransferWise payment method.\n\n\ -The TransferWise-USD payment method allows Haveno users to trade in USD.\n\n\ -Anyone with a Wise, formally TransferWise account, can add TransferWise-USD as a payment method in Haveno. This will \ + than most other currencies. For this reason USD was not added to Haveno Wise payment method.\n\n\ +The Wise-USD payment method allows Haveno users to trade in USD.\n\n\ +Anyone with a Wise, formally Wise account, can add Wise-USD as a payment method in Haveno. This will \ allow them to buy and sell XMR with USD.\n\n\ When trading on Haveno XMR Buyers should not use any reference for reason for payment. If reason for payment is required \ - they should only use the full name of the TransferWise-USD account owner. -payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the XMR Seller's Haveno TransferWise-USD account. -payment.transferwiseUsd.info.seller=Please check that the payment received matches the XMR Buyer's name of the TransferWise-USD account in Haveno. + they should only use the full name of the Wise-USD account owner. +payment.transferwiseUsd.info.buyer=Please send payment only to the email address in the XMR Seller's Haveno Wise-USD account. +payment.transferwiseUsd.info.seller=Please check that the payment received matches the XMR Buyer's name of the Wise-USD account in Haveno. payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\ \n\ @@ -2953,8 +3022,8 @@ payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on - XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\ \n\ In the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\n\ -Failure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\n\ -In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\n\ +Failure to provide the required information to the arbitrator will result in losing the dispute case.\n\n\ +In all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the arbitrator.\n\n\ If you do not understand these requirements, do not trade using USPMO on Haveno. payment.payByMail.info=Trading using Pay by Mail on Haveno requires that you understand the following:\n\ @@ -2985,11 +3054,13 @@ Any special terms/conditions; \n\ Any other details. payment.tradingRestrictions=Please review the maker's terms and conditions.\n\ If you do not meet the requirements do not take this trade. -payment.cashAtAtm.info=Cash at ATM: Cardless withdraw at ATM using code\n\n\ +payment.cashAtAtm.info=Cardless Cash: Cardless withdraw at ATM using code\n\n\ To use this payment method:\n\n\ - 1. Create a Cash at ATM payment account, lising your accepted banks, regions, or other terms to be shown with the offer.\n\n\ + 1. Create a Cardless Cash payment account, listing your accepted banks, regions, or other terms to be shown with the offer.\n\n\ 2. Create or take an offer with the payment account.\n\n\ 3. When the offer is taken, chat with your peer to coordinate a time to complete the payment and share the payment details.\n\n\ + If the trade amount is above the cash withdrawal limit, traders should split it into multiple transactions.\n\n\ + ATM cash trades must be in multiples of 10. Using range offers is recommended so the XMR amount can adjust to match the exact price.\n\n\ If you cannot complete the transaction as specified in your trade contract, you may lose some (or all) of your security deposit. payment.cashAtAtm.extraInfo.prompt=Please state on your offers: \n\n\ Your accepted banks / locations; \n\ @@ -3001,21 +3072,24 @@ payment.f2f.city=City for 'Face to face' meeting payment.f2f.city.prompt=The city will be displayed with the offer payment.shared.optionalExtra=Optional additional information payment.shared.extraInfo=Additional information -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Additional offer information +payment.shared.extraInfo.prompt.paymentAccount=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.prompt.offer=Define any special terms, conditions, or details you would like to be displayed with your offer. +payment.shared.extraInfo.noDeposit=Contact details and offer terms payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\n\ The main differences are:\n\ ● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n\ ● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n\ ● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n\ ● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n\ - ● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence \ + ● In case of a dispute the arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence \ of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ - recommendations at: [HYPERLINK:https://haveno.exchange/wiki/Face-to-face_(payment_method)] + recommendations at: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Open web page payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Additional information: {0} +payment.shared.extraInfo.tooltip=Additional information: {0} payment.ifsc=IFS Code payment.ifsc.validation=IFSC format: XXXX0999999 @@ -3029,12 +3103,15 @@ payment.payid.info=A PayID like a phone number, email address or an Australian B 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\n\ - Please see the wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] for further details and best practices. \n\n\ + 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 \ 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\ + Transactions made via PINs cannot be independently verified for dispute resolution. If an issue arises, recovering funds may not be possible.\n\n\ + To ensure transaction security with dispute resolution, always use payment methods that provide verifiable records. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -3046,7 +3123,7 @@ SPECIFIC_BANKS=Transfers with specific banks US_POSTAL_MONEY_ORDER=US Postal Money Order CASH_DEPOSIT=Cash Deposit PAY_BY_MAIL=Pay By Mail -CASH_AT_ATM=Cash at ATM +CASH_AT_ATM=Cardless Cash MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Face to face (in person) @@ -3066,7 +3143,7 @@ CASH_DEPOSIT_SHORT=Cash Deposit # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Pay By Mail # suppress inspection "UnusedProperty" -CASH_AT_ATM_SHORT=Cash At ATM +CASH_AT_ATM_SHORT=Cardless Cash # suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" @@ -3116,9 +3193,9 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD=TransferWise-USD +TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" @@ -3166,14 +3243,16 @@ DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap -# Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo +# suppress inspection "UnusedProperty" PAYPAL=PayPal +# suppress inspection "UnusedProperty" +PAYSAFE=Paysafe # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold @@ -3212,9 +3291,9 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD_SHORT=TransferWise-USD +TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" @@ -3269,7 +3348,10 @@ OK_PAY_SHORT=OKPay CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo +# suppress inspection "UnusedProperty" PAYPAL_SHORT=PayPal +# suppress inspection "UnusedProperty" +PAYSAFE_SHORT=Paysafe #################################################################### diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 18b25963c6..16b6a05bba 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -28,11 +28,14 @@ shared.readMore=Přečíst více shared.openHelp=Otevřít nápovědu shared.warning=Varování shared.close=Zavřít +shared.closeAnywayDanger=Přesto vypnout (NEBEZPEČNÉ!) +shared.okWait=Dobře, počkám shared.cancel=Zrušit shared.ok=OK shared.yes=Ano shared.no=Ne shared.iUnderstand=Rozumím +shared.continueAnyway=Přesto pokračovat shared.na=N/A shared.shutDown=Vypnout shared.reportBug=Nahlásit chybu na GitHubu @@ -40,6 +43,8 @@ shared.buyMonero=Koupit monero shared.sellMonero=Prodat monero shared.buyCurrency=Koupit {0} shared.sellCurrency=Prodat {0} +shared.buyCurrency.locked=Koupit {0} 🔒 +shared.sellCurrency.locked=Prodat {0} 🔒 shared.buyingXMRWith=nakoupit XMR za {0} shared.sellingXMRFor=prodat XMR za {0} shared.buyingCurrency=nakoupit {0} (prodat XMR) @@ -65,8 +70,8 @@ shared.fixedPriceInCurForCur=Pevná cena v {0} za 1 {1} shared.amount=Množství shared.txFee=Transakční poplatek shared.tradeFee=Obchodní poplatek -shared.buyerSecurityDeposit=Vklad kupujícího -shared.sellerSecurityDeposit=Vklad prodejce +shared.buyerSecurityDeposit=Vklad kauce kupujícího +shared.sellerSecurityDeposit=Vklad kauce prodejce shared.amountWithCur=Množství v {0} shared.volumeWithCur=Objem v {0} shared.currency=Měna @@ -81,12 +86,13 @@ shared.balanceWithCur=Zůstatek v {0} shared.utxo=Nevyčerpaný transakční výstup shared.txId=ID transakce shared.confirmations=Potvrzení -shared.revert=Návratová Tx +shared.revert=Vzít zpět Tx shared.select=Vybrat shared.usage=Použití shared.state=Stav shared.tradeId=ID obchodu shared.offerId=ID nabídky +shared.traderId=ID obchodníka shared.bankName=Jméno banky shared.acceptedBanks=Přijímané banky shared.amountMinMax=Množství (min - max) @@ -98,23 +104,27 @@ shared.removeOffer=Odstranit nabídku shared.dontRemoveOffer=Neodstraňovat nabídku shared.editOffer=Upravit nabídku shared.openLargeQRWindow=Otevřít velké okno s QR kódem -shared.tradingAccount=Obchodní účet +shared.chooseTradingAccount=Vyberte obchodní účet shared.faq=Navštívit stránku FAQ shared.yesCancel=Ano, zrušit shared.nextStep=Další krok -shared.selectTradingAccount=Vyberte obchodní účet -shared.fundFromSavingsWalletButton=Přesunout finance z Haveno peněženky +shared.fundFromSavingsWalletButton=Použít prostředky z peněženky Haveno shared.fundFromExternalWalletButton=Otevřít vaši externí peněženku pro financování -shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci moneroové peněženky. Jste si jisti, že máte nějakou nainstalovanou? +shared.openDefaultWalletFailed=Nepodařilo se otevřít aplikaci peněženky Monero. Jste si jisti, že máte nějakou nainstalovanou? shared.belowInPercent=% pod tržní cenou shared.aboveInPercent=% nad tržní cenou shared.enterPercentageValue=Zadejte % hodnotu shared.OR=NEBO shared.notEnoughFunds=Ve své peněžence Haveno nemáte pro tuto transakci dostatek prostředků — je potřeba {0}, ale k dispozici je pouze {1}.\n\nPřidejte prostředky z externí peněženky nebo financujte svou peněženku Haveno v části Prostředky > Přijmout prostředky. -shared.waitingForFunds=Čekání na finance... +shared.waitingForFunds=Čekání na finanční prostředky... +shared.yourDepositTransactionId=ID vaší vkladové transakce +shared.peerDepositTransactionId=ID vkladové transakce peera +shared.makerDepositTransactionId=ID vkladové transakce tvůrce +shared.takerDepositTransactionId=ID vkladové transakce příjemce shared.TheXMRBuyer=XMR kupující shared.You=Vy -shared.sendingConfirmation=Posílám potvrzení... +shared.preparingConfirmation=Příprava potvrzení... +shared.sendingConfirmation=Odesílání potvrzení... shared.sendingConfirmationAgain=Prosím pošlete potvrzení znovu shared.exportCSV=Exportovat do CSV shared.exportJSON=Exportovat do JSON @@ -123,9 +133,11 @@ shared.noDateAvailable=Žádné datum není k dispozici shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum +shared.sendFundsDetailsWithFee=Odesílání: {0}\n\nNa přijímací adresu: {1}\n\nDalší poplatek pro těžaře: {2}\n\nJste si jisti, že chcete vyplatit tuto částku? # suppress inspection "TrailingSpacesInProperty" -shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro moneroový konsenzus). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n +shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky +shared.copiedToClipboard=Zkopírováno do schránky! shared.language=Jazyk shared.country=Země shared.applyAndShutDown=Potvrdit a ukončit @@ -139,6 +151,7 @@ shared.addNewAccount=Přidat nový účet shared.ExportAccounts=Exportovat účty shared.importAccounts=Importovat účty shared.createNewAccount=Vytvořit nový účet +shared.createNewAccountDescription=Vaše údaje o účtu jsou uloženy místně na vašem zařízení a sdíleny pouze s vaším obchodním partnerem a rozhodcem, pokud dojde k otevření sporu. shared.saveNewAccount=Uložit nový účet shared.selectedAccount=Vybraný účet shared.deleteAccount=Smazat účet @@ -162,6 +175,7 @@ shared.acceptedTakerCountries=Země příjemce akceptovány shared.tradePrice=Tržní cena shared.tradeAmount=Výše obchodu shared.tradeVolume=Objem obchodu +shared.reservedAmount=Rezervovaná částka shared.invalidKey=Vložený klíč není správný shared.enterPrivKey=Pro odemknutí vložte privátní klíč shared.payoutTxId=ID platební transakce @@ -185,13 +199,17 @@ shared.total=Celkem shared.totalsNeeded=Potřebné prostředky shared.tradeWalletAddress=Adresa obchodní peněženky shared.tradeWalletBalance=Zůstatek obchodní peněženky -shared.reserveExactAmount=Rezervujte pouze potřebné finanční prostředky. Před aktivací vaší nabídky může být vyžadována těžební poplatek a 10 potvrzení (~20 minut). +shared.reserveExactAmount=Rezervujte pouze nezbytné prostředky. Vyžaduje poplatek za těžbu a přibližně 20 minut, než vaše nabídka půjde živě. shared.makerTxFee=Tvůrce: {0} shared.takerTxFee=Příjemce: {0} shared.iConfirm=Potvrzuji -shared.openURL=Otevřené {0} +shared.openURL=Otevřít {0} shared.fiat=Fiat shared.crypto=Krypto +shared.traditional=Tradiční +shared.otherAssets=jiná aktiva +shared.other=Jiné +shared.preciousMetals=Drahé kovy shared.all=Vše shared.edit=Upravit shared.advancedOptions=Pokročilé možnosti @@ -209,12 +227,16 @@ shared.mediator=Mediátor shared.arbitrator=Rozhodce shared.refundAgent=Rozhodce shared.refundAgentForSupportStaff=Rozhodce pro vrácení peněz -shared.delayedPayoutTxId=ID odložené platební transakce -shared.delayedPayoutTxReceiverAddress=Odložená výplatní transakce odeslána na +shared.delayedPayoutTxId=ID zpožděné platební transakce +shared.delayedPayoutTxReceiverAddress=Zpožděná výplatní transakce odeslána na shared.unconfirmedTransactionsLimitReached=Momentálně máte příliš mnoho nepotvrzených transakcí. Prosím zkuste to znovu později. shared.numItemsLabel=Počet položek: {0} shared.filter=Filtr shared.enabled=Aktivní +shared.pending=Otevřené +shared.me=Já +shared.maker=Tvůrce +shared.taker=Příjemce #################################################################### @@ -229,14 +251,15 @@ mainView.menu.market=Trh mainView.menu.buyXmr=Koupit XMR mainView.menu.sellXmr=Prodat XMR mainView.menu.portfolio=Portfolio -mainView.menu.funds=Finance +mainView.menu.funds=Prostředky mainView.menu.support=Podpora mainView.menu.settings=Nastavení mainView.menu.account=Účet mainView.marketPriceWithProvider.label=Tržní cena {0} mainView.marketPrice.havenoInternalPrice=Cena posledního Haveno obchodu -mainView.marketPrice.tooltip.havenoInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\nZobrazená cena je nejnovější obchodní cena Haveno pro tuto měnu. +mainView.marketPrice.tooltip.havenoInternalPrice=Neexistují tržní ceny od externích poskytovatelů cenových feedů.\n\ +Zobrazená cena je nejnovější obchodní cena Haveno pro tuto měnu. mainView.marketPrice.tooltip=Tržní cena je poskytována {0}{1}\nPoslední aktualizace: {2}\nURL uzlu poskytovatele: {3} mainView.balance.available=Dostupný zůstatek mainView.balance.reserved=Rezervováno v nabídkách @@ -248,7 +271,7 @@ mainView.footer.usingTor=(přes Tor) mainView.footer.localhostMoneroNode=(localhost) mainView.footer.clearnet=(přes clearnet) mainView.footer.xmrInfo={0} {1} -mainView.footer.xmrFeeRate=/ Aktuální poplatek: {0} sat/vB +mainView.footer.xmrFeeRate=/ Míra poplatku: {0} sat/vB mainView.footer.xmrInfo.initializing=Připojování k síti Haveno mainView.footer.xmrInfo.synchronizingWith=Synchronizace s {0} na bloku: {1} / {2} mainView.footer.xmrInfo.connectedTo=Připojeno k {0} v bloku {1} @@ -256,19 +279,19 @@ mainView.footer.xmrInfo.synchronizingWalletWith=Synchronizace peněženky s {0} mainView.footer.xmrInfo.syncedWith=Synchronizováno s {0} na bloku {1} mainView.footer.xmrInfo.connectingTo=Připojování mainView.footer.xmrInfo.connectionFailed=Připojení se nezdařilo -mainView.footer.xmrPeers=Monero síťové nody: {0} -mainView.footer.p2pPeers=Haveno síťové nody: {0} +mainView.footer.xmrPeers=Monero síťové uzly: {0} +mainView.footer.p2pPeers=Haveno síťové uzly: {0} mainView.bootstrapState.connectionToTorNetwork=(1/4) Připojování do sítě Tor... -mainView.bootstrapState.torNodeCreated=(2/4) Tor node vytvořen -mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba publikována +mainView.bootstrapState.torNodeCreated=(2/4) Tor uzel vytvořen +mainView.bootstrapState.hiddenServicePublished=(3/4) Skrytá služba zveřejněna mainView.bootstrapState.initialDataReceived=(4/4) Iniciační data přijata -mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed nody nejsou k dispozici -mainView.bootstrapWarning.noNodesAvailable=Žádné seed ani peer nody k dispozici +mainView.bootstrapWarning.noSeedNodesAvailable=Žádné seed uzly nejsou k dispozici +mainView.bootstrapWarning.noNodesAvailable=Žádné seed ani peer uzly k dispozici mainView.bootstrapWarning.bootstrappingToP2PFailed=Zavádění do sítě Haveno se nezdařilo -mainView.p2pNetworkWarnMsg.noNodesAvailable=Pro vyžádání dat nejsou k dispozici žádné seed ani peer nody.\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. +mainView.p2pNetworkWarnMsg.noNodesAvailable=Pro vyžádání dat nejsou k dispozici žádné seed ani peer uzly.\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.p2pNetworkWarnMsg.connectionToP2PFailed=Připojení k síti Haveno selhalo (nahlášená chyba: {0}).\nZkontrolujte připojení k internetu nebo zkuste aplikaci restartovat. mainView.walletServiceErrorMsg.timeout=Připojení k síti Monero selhalo kvůli vypršení časového limitu. @@ -276,9 +299,10 @@ mainView.walletServiceErrorMsg.connectionError=Připojení k síti Monero selhal mainView.walletServiceErrorMsg.rejectedTxException=Transakce byla ze sítě zamítnuta.\n\n{0} -mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer nodům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. -mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k Moneroovému localhost nodu.\nRestartujte aplikaci Haveno a připojte se k jiným Moneroovým nodům nebo restartujte Moneroový localhost node. +mainView.networkWarning.allConnectionsLost=Ztratili jste připojení ke všem {0} síťovým peer uzlům.\nMožná jste ztratili připojení k internetu nebo byl váš počítač v pohotovostním režimu. +mainView.networkWarning.localhostMoneroLost=Ztratili jste připojení k localhost uzlu Monero.\nRestartujte aplikaci Haveno a připojte se k jiným uzlům Monero nebo restartujte localhost Monero uzel. mainView.version.update=(Dostupná aktualizace) +mainView.status.connections=Příchozí připojení: {0}nOdchozí připojení: {1} #################################################################### @@ -290,11 +314,10 @@ market.tabs.spreadCurrency=Nabídky podle měn market.tabs.spreadPayment=Nabídky podle způsobů platby market.tabs.trades=Obchody +# OfferBookView +market.offerBook.filterPrompt=Filtr + # OfferBookChartView -market.offerBook.buyCrypto=Koupit {0} (prodat {1}) -market.offerBook.sellCrypto=Prodat {0} (koupit {1}) -market.offerBook.buyWithTraditional=Koupit {0} -market.offerBook.sellWithTraditional=Prodat {0} market.offerBook.sellOffersHeaderLabel=Prodat {0} kupujícímu market.offerBook.buyOffersHeaderLabel=Koupit {0} od prodejce market.offerBook.buy=Chci koupit monero @@ -326,20 +349,22 @@ market.trades.showVolumeInUSD=Zobrazit objem v USD offerbook.createOffer=Vytvořit nabídku offerbook.takeOffer=Přijmout nabídku -offerbook.takeOfferToBuy=Přijmout nabídku na nákup {0} -offerbook.takeOfferToSell=Přijmout nabídku k prodeji {0} +offerbook.takeOffer.createAccount=Vytvořit účet a přijmout nabídku +offerbook.takeOffer.enterChallenge=Zadejte heslo nabídky offerbook.trader=Obchodník offerbook.offerersBankId=ID banky tvůrce (BIC/SWIFT): {0} offerbook.offerersBankName=Jméno banky tvůrce: {0} offerbook.offerersBankSeat=Sídlo banky tvůrce: {0} offerbook.offerersAcceptedBankSeatsEuro=Přijatá sídla bank (příjemce): Všechny země Eura offerbook.offerersAcceptedBankSeats=Přijatá sídla bank (příjemce):\n {0} -offerbook.availableOffers=Dostupné nabídky +offerbook.availableOffersToBuy=Koupit {0} pomocí {1} +offerbook.availableOffersToSell=Prodat {0} za {1} offerbook.filterByCurrency=Filtrovat podle měny offerbook.filterByPaymentMethod=Filtrovat podle platební metody offerbook.matchingOffers=Nabídky odpovídající mým účtům +offerbook.filterNoDeposit=Žádný vklad +offerbook.noDepositOffers=Nabídky bez zálohy (vyžaduje se heslo) offerbook.timeSinceSigning=Informace o účtu -offerbook.timeSinceSigning.info=Tento účet byl ověřen a {0} offerbook.timeSinceSigning.info.arbitrator=podepsán rozhodcem a může podepisovat účty partnerů offerbook.timeSinceSigning.info.peer=podepsáno partnerem, nyní čeká ještě %d dnů na zrušení limitů offerbook.timeSinceSigning.info.peerLimitLifted=podepsán partnerem a limity byly zrušeny @@ -347,37 +372,42 @@ offerbook.timeSinceSigning.info.signer=podepsán partnerem a může podepsat ú offerbook.timeSinceSigning.info.banned=účet byl zablokován offerbook.timeSinceSigning.daysSinceSigning={0} dní offerbook.timeSinceSigning.daysSinceSigning.long={0} od podpisu +offerbook.timeSinceSigning.tooltip.accountLimit=Limit účtu: {0} +offerbook.timeSinceSigning.tooltip.accountLimitLifted=Limit účtu odebrán +offerbook.timeSinceSigning.tooltip.info.unsigned=Tento účet ještě nebyl podepsán +offerbook.timeSinceSigning.tooltip.info.signed=Tento účet byl podepsán +offerbook.timeSinceSigning.tooltip.info.signedAndLifted=Tento účet byl podepsán a může podepisovat účty peerů +offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=nakoupeno XMR od podepsaného účtu +offerbook.timeSinceSigning.tooltip.checkmark.wait=čekáno alespoň {0} dnů +offerbook.timeSinceSigning.tooltip.learnMore=Více informací offerbook.xmrAutoConf=Je automatické potvrzení povoleno +offerbook.buyXmrWith=Koupit XMR za: +offerbook.sellXmrFor=Prodat XMR za: -offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n{0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů. +offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n\ + {0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů. offerbook.timeSinceSigning.notSigned=Dosud nepodepsáno offerbook.timeSinceSigning.notSigned.ageDays={0} dní offerbook.timeSinceSigning.notSigned.noNeed=N/A -shared.notSigned=Tento účet ještě nebyl podepsán a byl vytvořen před {0} dny -shared.notSigned.noNeed=Tento typ účtu nevyžaduje podepisování shared.notSigned.noNeedDays=Tento typ účtu nevyžaduje podepisování a byl vytvořen před {0} dny -shared.notSigned.noNeedAlts=Cryptoové účty neprocházejí kontrolou podpisu a stáří +shared.notSigned.noNeedAlts=Kryptoměnové účty neprocházejí kontrolou podpisu a stáří offerbook.nrOffers=Počet nabídek: {0} offerbook.volume={0} (min - max) offerbook.deposit=Kauce XMR (%) offerbook.deposit.help=Kauce zaplacená každým obchodníkem k zajištění obchodu. Bude vrácena po dokončení obchodu. -offerbook.createOfferToBuy=Vytvořit novou nabídku k nákupu {0} -offerbook.createOfferToSell=Vytvořit novou nabídku k prodeji {0} -offerbook.createOfferToBuy.withTraditional=Vytvořit novou nabídku k nákupu {0} za {1} -offerbook.createOfferToSell.forTraditional=Vytvořit novou nabídku k prodeji {0} za {1} -offerbook.createOfferToBuy.withCrypto=Vytvořit novou nabídku k prodeji {0} (koupit {1}) -offerbook.createOfferToSell.forCrypto=Vytvořit novou nabídku na nákup {0} (prodat {1}) +offerbook.createNewOffer=Vytvořit nabídku pro {0} {1} +offerbook.createOfferDisabled.tooltip=Můžete vytvořit zároveň jen jednu nabídku -offerbook.takeOfferButton.tooltip=Využijte nabídku {0} -offerbook.yesCreateOffer=Ano, vytvořit nabídku +offerbook.takeOfferButton.tooltip=Využít nabídku {0} offerbook.setupNewAccount=Založit nový obchodní účet offerbook.removeOffer.success=Odebrání nabídky bylo úspěšné. offerbook.removeOffer.failed=Odebrání nabídky selhalo:\n{0} offerbook.deactivateOffer.failed=Deaktivace nabídky se nezdařila:\n{0} offerbook.activateOffer.failed=Zveřejnění nabídky se nezdařilo:\n{0} -offerbook.withdrawFundsHint=Prostředky, které jste zaplatili, můžete vybrat z obrazovky {0}. +offerbook.withdrawFundsHint=Nabídka byla odebrána. Prostředky již nejsou pro tuto nabídku rezervovány. \n + Dostupné prostředky můžete odeslat do své externí peněženky prostřednictvím {0}. offerbook.warning.noTradingAccountForCurrency.headline=Žádný platební účet pro vybranou měnu offerbook.warning.noTradingAccountForCurrency.msg=Pro vybranou měnu nemáte nastavený platební účet.\n\nChcete místo toho vytvořit nabídku pro jinou měnu? @@ -386,10 +416,18 @@ offerbook.warning.noMatchingAccount.msg=Tato nabídka používá platební metod offerbook.warning.counterpartyTradeRestrictions=Tuto nabídku nelze přijmout z důvodu obchodních omezení protistrany -offerbook.warning.newVersionAnnouncement=S touto verzí softwaru mohou obchodní partneři navzájem ověřovat a podepisovat platební účty ostatních a vytvářet tak síť důvěryhodných platebních účtů.\n\nPo úspěšném obchodování s partnerským účtem s ověřeným platebním účtem bude váš platební účet podepsán a obchodní limity budou zrušeny po určitém časovém intervalu (délka tohoto intervalu závisí na způsobu ověření).\n\nDalší informace o podepsání účtu naleznete v dokumentaci na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. +offerbook.warning.newVersionAnnouncement=S touto verzí softwaru mohou obchodní partneři navzájem ověřovat a podepisovat platební účty ostatních a vytvářet tak síť důvěryhodných platebních účtů.\n\n +Po úspěšném obchodování s partnerským účtem s ověřeným platebním účtem bude váš platební účet podepsán a obchodní limity budou zrušeny po určitém časovém intervalu (délka tohoto intervalu závisí na způsobu ověření).\n\n +Další informace o podepsání účtu naleznete v dokumentaci na adrese [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing]. -popup.warning.tradeLimitDueAccountAgeRestriction.seller=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n- Účet kupujícího nebyl podepsán rozhodcem ani obchodním partnerem\n- Doba od podpisu účtu kupujícího není alespoň 30 dní\n- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} -popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n- Váš účet nebyl podepsán rozhodcem ani obchodním partnerem\n- Čas od podpisu vašeho účtu není alespoň 30 dní\n- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.seller=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ +- Účet kupujícího nebyl podepsán rozhodcem ani obchodním partnerem\n\ +- Doba od podpisu účtu kupujícího není alespoň 30 dní\n\ +- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} +popup.warning.tradeLimitDueAccountAgeRestriction.buyer=Povolená částka obchodu je omezena na {0} z důvodu bezpečnostních omezení na základě následujících kritérií:\n\ +- Váš účet nebyl podepsán rozhodcem ani obchodním partnerem\n\ +- Čas od podpisu vašeho účtu není alespoň 30 dní\n\ +- Způsob platby této nabídky je považován za riskantní pro bankovní zpětné zúčtování\n\n{1} popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=Tento platební metoda je dočasně omezena na {0} do {1}, protože všichni kupující mají nové účty.\n\n{2} popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Vaše nabídka bude omezena na kupující s podepsanými a starými účty, protože překračuje {0}.\n\n{1} @@ -399,8 +437,13 @@ offerbook.warning.offerBlocked=Tato nabídka byla blokována vývojáři Haveno. offerbook.warning.currencyBanned=Měna použitá v této nabídce byla blokována vývojáři Haveno.\nDalší informace naleznete na fóru Haveno. offerbook.warning.paymentMethodBanned=Vývojáři Haveno zablokovali způsob platby použitý v této nabídce.\nDalší informace naleznete na fóru Haveno. offerbook.warning.nodeBlocked=Onion adresa tohoto obchodníka byla zablokována vývojáři Haveno.\nPravděpodobně existuje neošetřená chyba způsobující problémy při přijímání nabídek od tohoto obchodníka. -offerbook.warning.requireUpdateToNewVersion=Vaše verze Haveno již není kompatibilní pro obchodování. Aktualizujte prosím na nejnovější verzi Haveno na adrese [HYPERLINK:https://haveno.exchange/downloads]. -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.requireUpdateToNewVersion=Vaše verze Haveno již není kompatibilní pro obchodování. + Aktualizujte prosím na nejnovější verzi Haveno. +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.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). offerbook.info.buyAtMarketPrice=Budete nakupovat za tržní cenu (aktualizováno každou minutu). @@ -410,25 +453,28 @@ offerbook.info.sellAboveMarketPrice=Získáte o {0} více, než je aktuální tr offerbook.info.buyBelowMarketPrice=Platíte o {0} méně, než je aktuální tržní cena (aktualizováno každou minutu). offerbook.info.buyAtFixedPrice=Budete nakupovat za tuto pevnou cenu. offerbook.info.sellAtFixedPrice=Budete prodávat za tuto pevnou cenu. -offerbook.info.noArbitrationInUserLanguage=V případě sporu mějte na paměti, že arbitráž pro tuto nabídku bude řešit {0}. Jazyk je aktuálně nastaven na {1}. offerbook.info.roundedFiatVolume=Částka byla zaokrouhlena, aby se zvýšilo soukromí vašeho obchodu. #################################################################### # Offerbook / Create offer #################################################################### -createOffer.amount.prompt=Vložte množství v XMR +createOffer.amount.prompt=Zadejte množství v XMR createOffer.price.prompt=Zadejte cenu -createOffer.volume.prompt=Vložte množství v {0} +createOffer.volume.prompt=Zadejte množství v {0} createOffer.amountPriceBox.amountDescription=Množství XMR, které chcete {0} -createOffer.amountPriceBox.buy.volumeDescription=Částka v {0}, kterou utratíte -createOffer.amountPriceBox.sell.volumeDescription=Částka v {0}, kterou přijmete +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=Financujte svou nabídku +createOffer.fundsBox.title=Financovat nabídku createOffer.fundsBox.offerFee=Obchodní poplatek createOffer.fundsBox.networkFee=Poplatek za těžbu -createOffer.fundsBox.placeOfferSpinnerInfo=Probíhá publikování nabídky ... +createOffer.fundsBox.placeOfferSpinnerInfo=Probíhá zveřejnění nabídky ... createOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} createOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) createOffer.success.headline=Vaše nabídka byla vytvořena @@ -440,42 +486,55 @@ createOffer.info.buyBelowMarketPrice=Vždy zaplatíte o {0} % méně, než je ak createOffer.warning.sellBelowMarketPrice=Vždy získáte o {0} % méně, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.warning.buyAboveMarketPrice=Vždy zaplatíte o {0} % více, než je aktuální tržní cena, protože cena vaší nabídky bude průběžně aktualizována. createOffer.tradeFee.descriptionXMROnly=Obchodní poplatek -createOffer.tradeFee.descriptionBSQEnabled=Zvolte měnu obchodního poplatku +createOffer.tradeFee.description=Obchodní poplatek createOffer.triggerPrice.prompt=Nepovinná limitní cena createOffer.triggerPrice.label=Deaktivovat nabídku, pokud tržní cena dosáhne {0} -createOffer.triggerPrice.tooltip=Abyste se ochránili před prudkými výkyvy tržních cen, můžete nastavit limitní cenu, po jejímž dosažení bude vaše nabídka stažena. +createOffer.triggerPrice.tooltip=Abyste se ochránili před prudkými výkyvy tržních cen, můžete nastavit limitní cenu, \ +po jejímž dosažení bude vaše nabídka stažena. 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=Přehled: Umístěte nabídku {0} monero -createOffer.createOfferFundWalletInfo.headline=Financujte svou nabídku +createOffer.placeOfferButton=Zkontrolovat vytvoření nabídky {0} monero +createOffer.placeOfferButtonCrypto=Zkontrolovat vytvoření nabídky {0} {1} +createOffer.createOfferFundWalletInfo.headline=Financovat nabídku # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0}\n -createOffer.createOfferFundWalletInfo.msg=Do této nabídky musíte vložit {0}.\n\nTyto prostředky jsou rezervovány ve vaší lokální peněžence a budou uzamčeny na vkladové multisig adrese, jakmile někdo příjme vaši nabídku.\n\nČástka je součtem:\n{1}- Vaše kauce: {2}\n- Obchodní poplatek: {3}\n- Poplatek za těžbu: {4}\n\nPři financování obchodu si můžete vybrat ze dvou možností:\n- Použijte svou peněženku Haveno (pohodlné, ale transakce mohou být propojitelné) NEBO\n- Přenos z externí peněženky (potenciálně více soukromé)\n\nPo uzavření tohoto vyskakovacího okna se zobrazí všechny možnosti a podrobnosti financování. +createOffer.createOfferFundWalletInfo.msg=Potřebujete vložit {0} do této nabídky.\n\n\ + Tyto prostředky jsou rezervovány ve vaší místní peněžence a budou zablokovány v multisig peněžence, jakmile někdo přijme vaši nabídku.\n\n\ + Částka je součtem:\n\ + {1}\ + - Vaše záloha: {2}\n\ + - Poplatek za obchodování: {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=Při zadávání nabídky došlo k chybě:\n\n{0}\n\nPeněženku ještě neopustily žádné finanční prostředky.\nRestartujte aplikaci a zkontrolujte síťové připojení. +createOffer.amountPriceBox.error.message=Při zadávání nabídky došlo k chybě:\n\n{0}\n\n +Peněženku ještě neopustily žádné finanční prostředky.\n\ +Restartujte aplikaci a zkontrolujte síťové připojení. createOffer.setAmountPrice=Nastavit množství a cenu -createOffer.warnCancelOffer=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce "Prostředky/Odeslat prostředky". Opravdu si přejete zrušit? +createOffer.warnCancelOffer=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? createOffer.timeoutAtPublishing=Při zveřejnění nabídky došlo k vypršení časového limitu. createOffer.errorInfo=\n\nTvůrčí poplatek je již zaplacen. V nejhorším případě jste tento poplatek ztratili.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. -createOffer.tooLowSecDeposit.warning=Nastavili jste kauci na nižší hodnotu, než je doporučená výchozí hodnota {0}.\nOpravdu chcete použít nižší kauci? +createOffer.tooLowSecDeposit.warning=Nastavili jste kauci na nižší hodnotu, než je doporučená výchozí hodnota {0}.\n + Opravdu chcete použít nižší kauci? createOffer.tooLowSecDeposit.makerIsSeller=Poskytuje vám to menší ochranu v případě, že obchodní partner nedodrží obchodní protokol. -createOffer.tooLowSecDeposit.makerIsBuyer=Obchodní partner bude mít menší jistotu, že dodržíte obchodní protokol, protože uložená kauce bude příliš nízká. Ostatní uživatelé mohou raději využít jiné nabídky než té vaší. +createOffer.tooLowSecDeposit.makerIsBuyer=Obchodní partner bude mít menší jistotu, že dodržíte obchodní protokol, protože uložená kauce bude příliš nízká. \ + Ostatní uživatelé mohou raději využít jiné nabídky než té vaší. createOffer.resetToDefault=Ne, restartovat na výchozí hodnotu createOffer.useLowerValue=Ano, použijte moji nižší hodnotu createOffer.priceOutSideOfDeviation=Cena, kterou jste zadali, je mimo max. povolenou odchylku od tržní ceny.\nMax. povolená odchylka je {0} a lze ji upravit v preferencích. createOffer.changePrice=Změnit cenu -createOffer.tac=Publikováním této nabídky souhlasím s obchodováním s jakýmkoli obchodníkem, který splňuje podmínky definované na této obrazovce. -createOffer.currencyForFee=Obchodní poplatek +createOffer.tac=Zveřejněním této nabídky souhlasím s obchodováním s jakýmkoli obchodníkem, který splňuje podmínky definované na této obrazovce. createOffer.setDeposit=Nastavit kauci kupujícího (%) createOffer.setDepositAsBuyer=Nastavit mou kauci jako kupujícího (%) createOffer.setDepositForBothTraders=Nastavit kauci obou obchodníků (%) -createOffer.securityDepositInfo=Kauce vašeho kupujícího bude {0} +createOffer.securityDepositInfo=Vaše kauce kupujícího bude {0} createOffer.securityDepositInfoAsBuyer=Vaše kauce jako kupující bude {0} -createOffer.minSecurityDepositUsed=Je použita min. záloha kupujícího +createOffer.minSecurityDepositUsed=Minimální bezpečnostní záloha je použita +createOffer.buyerAsTakerWithoutDeposit=Žádný vklad od kupujícího (chráněno heslem) +createOffer.myDeposit=Můj bezpečnostní vklad (%) +createOffer.myDepositInfo=Vaše záloha na bezpečnost bude {0} #################################################################### @@ -484,14 +543,16 @@ createOffer.minSecurityDepositUsed=Je použita min. záloha kupujícího takeOffer.amount.prompt=Vložte množství v XMR takeOffer.amountPriceBox.buy.amountDescription=Množství XMR na prodej -takeOffer.amountPriceBox.sell.amountDescription=Množství XMR k nákupu +takeOffer.amountPriceBox.sell.amountDescription=Množství XMR k nakoupení +takeOffer.amountPriceBox.buy.amountDescriptionCrypto=Množství XMR na prodej +takeOffer.amountPriceBox.sell.amountDescriptionCrypto=Množství XMR k nakoupení takeOffer.amountPriceBox.priceDescription=Cena za monero v {0} takeOffer.amountPriceBox.amountRangeDescription=Možný rozsah množství takeOffer.amountPriceBox.warning.invalidXmrDecimalPlaces=Částka, kterou jste zadali, přesahuje počet povolených desetinných míst.\nČástka byla upravena na 4 desetinná místa. takeOffer.validation.amountSmallerThanMinAmount=Částka nesmí být menší než minimální částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmount=Vstupní částka nesmí být vyšší než částka stanovená v nabídce. takeOffer.validation.amountLargerThanOfferAmountMinusFee=Toto vstupní množství by vytvořilo zanedbatelné drobné pro prodejce XMR. -takeOffer.fundsBox.title=Financujte svůj obchod +takeOffer.fundsBox.title=Financovat obchod takeOffer.fundsBox.isOfferAvailable=Kontroluje se, zda je nabídka k dispozici ... takeOffer.fundsBox.tradeAmount=Částka k prodeji takeOffer.fundsBox.offerFee=Obchodní poplatek @@ -499,23 +560,26 @@ takeOffer.fundsBox.networkFee=Celkové poplatky za těžbu takeOffer.fundsBox.takeOfferSpinnerInfo=Přijímám nabídku: {0} takeOffer.fundsBox.paymentLabel=Haveno obchod s ID {0} takeOffer.fundsBox.fundsStructure=(kauce {0}, obchodní poplatek {1}, poplatek za těžbu {2}) +takeOffer.fundsBox.noFundingRequiredTitle=Žádné financování požadováno +takeOffer.fundsBox.noFundingRequiredDescription=Získejte passphrase nabídky od prodávajícího mimo Haveno, abyste tuto nabídku přijali. takeOffer.success.headline=Úspěšně jste přijali nabídku. takeOffer.success.info=Stav vašeho obchodu můžete vidět v \"Portfolio/Otevřené obchody\". takeOffer.error.message=Při převzetí nabídky došlo k chybě.\n\n{0} # new entries -takeOffer.takeOfferButton=Přehled: Využijte nabídku {0} monero +takeOffer.takeOfferButton=Zkontrolovat přijetí nabídky {0} monero +takeOffer.takeOfferButtonCrypto=Zkontrolovat přijetí nabídky {0} {1} 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=Financujte svůj obchod +takeOffer.takeOfferFundWalletInfo.headline=Financovat obchod # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Výše obchodu: {0} \n takeOffer.takeOfferFundWalletInfo.msg=Abyste mohli tuto nabídku využít, musíte vložit {0}.\n\nČástka je součtem:\n{1} - Vaší kauce: {2}\n- Obchodního poplatku: {3}\n- Celkového poplatku za těžbu: {4}\n\nPři financování obchodu si můžete vybrat ze dvou možností:\n- Použijte svou peněženku Haveno (pohodlné, ale transakce mohou být propojitelné) NEBO\n- Platba z externí peněženky (potenciálně více soukromé)\n\nPo uzavření tohoto vyskakovacího okna se zobrazí všechny možnosti a podrobnosti financování. takeOffer.alreadyPaidInFunds=Pokud jste již prostředky zaplatili, můžete je vybrat na obrazovce \"Prostředky/Odeslat prostředky\". -takeOffer.paymentInfo=Informace o platbě takeOffer.setAmountPrice=Nastavit částku -takeOffer.alreadyFunded.askCancel=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce "Prostředky/Odeslat prostředky". Opravdu si přejete zrušit? +takeOffer.alreadyFunded.askCancel=Tuto nabídku jste již financovali. Pokud ji nyní zrušíte, zůstanou vaše prostředky v místní peněžence Haveno a budou k dispozici pro výběr na obrazovce \"Prostředky/Odeslat prostředky\". Opravdu si přejete zrušit? takeOffer.failed.offerNotAvailable=Žádost o nabídku se nezdařila, protože nabídka již není k dispozici. Možná, že mezitím nabídku přijal jiný obchodník. takeOffer.failed.offerTaken=Tuto nabídku nemůžete přijmout, protože ji již přijal jiný obchodník. +takeOffer.failed.offerInvalid=Tuto nabídku nemůžete přijmout, protože podpis tvůrce je neplatný. takeOffer.failed.offerRemoved=Tuto nabídku nemůžete přijmout, protože mezitím byla nabídka odstraněna. takeOffer.failed.offererNotOnline=Přijetí nabídky se nezdařilo, protože tvůrce již není online. takeOffer.failed.offererOffline=Tuto nabídku nemůžete přijmout, protože je tvůrce offline. @@ -523,7 +587,7 @@ takeOffer.warning.connectionToPeerLost=Ztratili jste spojení s tvůrcem.\nMohli takeOffer.error.noFundsLost=\n\nPeněženku ještě neopustily žádné finanční prostředky.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. # suppress inspection "TrailingSpacesInProperty" -takeOffer.error.feePaid=.\n\n +takeOffer.error.feePaid=\n\n takeOffer.error.depositPublished=\n\nVkladová transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.error.payoutPublished=\n\nVyplacená transakce je již zveřejněna.\nZkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit.\nPokud problém přetrvává, kontaktujte vývojáře a požádejte je o podporu. takeOffer.tac=Přijetím této nabídky souhlasím s obchodními podmínkami definovanými na této obrazovce. @@ -535,11 +599,12 @@ takeOffer.tac=Přijetím této nabídky souhlasím s obchodními podmínkami def openOffer.header.triggerPrice=Limitní cena openOffer.triggerPrice=Limitní cena {0} -openOffer.triggered=Nabídka byla deaktivována, protože tržní cena dosáhla vámi stanovené limitní ceny.\nProsím nastavte novou limitní cenu ve vaší nabídce +openOffer.triggered=Nabídka byla deaktivována, protože tržní cena dosáhla vámi stanovené limitní ceny.\n\ + Prosím nastavte novou limitní cenu ve vaší nabídce editOffer.setPrice=Nastavit cenu editOffer.confirmEdit=Potvrdit: Upravit nabídku -editOffer.publishOffer=Publikování vaší nabídky. +editOffer.publishOffer=Zveřejnění vaší nabídky. editOffer.failed=Úprava nabídky se nezdařila:\n{0} editOffer.success=Vaše nabídka byla úspěšně upravena. editOffer.invalidDeposit=Kauce kupujícího není v rámci omezení definovaných Haveno DAO a nemůže být dále upravována. @@ -553,10 +618,21 @@ portfolio.tab.pendingTrades=Otevřené obchody portfolio.tab.history=Historie portfolio.tab.failed=Selhalo portfolio.tab.editOpenOffer=Upravit nabídku +portfolio.tab.duplicateOffer=Duplicitní nabídka +portfolio.context.offerLikeThis=Vytvořit novou nabídku jako je tato... +portfolio.context.notYourOffer=Duplikovat můžete pouze nabídky, u kterých jste byli tvůrcem. portfolio.closedTrades.deviation.help=Procentuální odchylka od tržní ceny -portfolio.pending.invalidTx=Došlo k problému s chybějící nebo neplatnou transakcí.\n\nProsím neposílejte fiat nebo crypto platby.\n\nOtevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\nChybová zpráva: {0} +portfolio.pending.invalidTx=Došlo k problému s chybějící nebo neplatnou transakcí.\n\n\ + Prosím neposílejte fiat nebo crypto platby.\n\n\ + Otevřete úkol pro podporu, některý z mediátorů vám pomůže.\n\n\ + Chybová zpráva: {0} + +portfolio.pending.unconfirmedTooLong=Vkladové transakce obchodu {0} jsou stále nepotvrzené po {1} hodinách. \ + Zkontrolujte transakce vkladu pomocí průzkumníka blockchainu; pokud jsou potvrzené, ale nezobrazují se jako \ + potvrzené v Haveno, zkuste Haveno restartovat.\n\n\ + 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.startPayment=Zahajte platbu @@ -595,32 +671,43 @@ portfolio.pending.autoConf.state.ERROR=Došlo k chybě při požadavku na služb # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Služba se vrátila se selháním. Není možné automatické potvrzení. -portfolio.pending.step1.info=Vkladová transakce byla zveřejněna.\n{0} před zahájením platby musíte počkat na alespoň jedno potvrzení na blockchainu. +portfolio.pending.step1.info.you=Transakce vkladu byla publikována.\nMusíte počkat na 10 potvrzení (přibližně 20 minut), než bude platba zahájena. +portfolio.pending.step1.info.buyer=Transakce vkladu byla publikována.\nKupující XMR musí počkat na 10 potvrzení (asi 20 minut), než bude platba zahájena. portfolio.pending.step1.warn=Vkladová transakce není stále potvrzena. K tomu někdy dochází ve vzácných případech, kdy byl poplatek za financování jednoho obchodníka z externí peněženky příliš nízký. -portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. Můžete počkat déle nebo požádat o pomoc mediátora. +portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. \ + Pokud jste čekali mnohem déle než 20 minut, můžete poádat o pomoc podporu Haveno. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Váš obchod má alespoň jedno potvrzení blockchainu.\n\n -portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platební informace, nechte pole \"důvod platby\" prázdné. NEPOUŽÍVEJTE ID obchodu ani jiné poznámky jako např. 'monero', 'XMR' nebo 'Haveno'. Můžete se se svým obchodním partnerem domluvit pomocí chatu na identifikaci platby, která bude vyhovovat oběma. +portfolio.pending.step2_buyer.refTextWarn=Důležité: když vyplňujete platební informace, nechte pole \"důvod platby\" \ + prázdné. NEPOUŽÍVEJTE ID obchodu ani jiné poznámky jako např. 'monero', 'XMR' nebo 'Haveno'. \ + Můžete se se svým obchodním partnerem domluvit pomocí chatu na \"důvod platby\", který bude vyhovovat oběma. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees=Pokud vaše banka účtuje poplatky za převod, musíte tyto poplatky uhradit vy. # suppress inspection "TrailingSpacesInProperty" +portfolio.pending.step2_buyer.fees.swift=Ujistěte se, že k odeslání platby SWIFT používáte model SHA (model sdílených poplatků). \ + Více detailů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option]. +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Převeďte prosím z vaší externí {0} peněženky\n{1} prodejci XMR.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Přejděte do banky a zaplaťte {0} prodejci XMR.\n\n portfolio.pending.step2_buyer.cash.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zapište na papírový doklad: NO REFUNDS - bez náhrady.\nPoté ji roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Zaplaťte prosím {0} prodejci XMR pomocí MoneyGram.\n\n -portfolio.pending.step2_buyer.moneyGram.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete autorizační číslo a fotografii s potvrzením e-mailem prodejci XMR.\nPotvrzení musí jasně uvádět celé jméno, zemi, stát a částku prodávajícího. E-mail prodejce je: {0}. +portfolio.pending.step2_buyer.moneyGram.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete autorizační číslo a fotografii s potvrzením e-mailem prodejci XMR.\n\ +Potvrzení musí jasně uvádět celé jméno, zemi, stát a částku prodávajícího. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Zaplaťte prosím {0} prodejci XMR pomocí Western Union.\n\n -portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci XMR e-mail s MTCN (sledovací číslo) a fotografii s potvrzením o přijetí.\nPotvrzení musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}. +portfolio.pending.step2_buyer.westernUnion.extra=DŮLEŽITÉ POŽADAVKY:\nPo provedení platby zašlete prodejci XMR e-mail s MTCN (sledovací číslo) a fotografii s potvrzením o přijetí.\n\ + Potvrzení musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Zašlete prosím {0} prodejci XMR pomocí \"US Postal Money Order\".\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Pay by Mail\"). Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. Více informací naleznete na Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Zašlete prosím {0} prodejci XMR v poštovní zásilce (\"Hotovost poštou\"). \ + Konkrétní instrukce naleznete v obchodní smlouvě. V případě pochybností se můžete zeptat protistrany pomocí obchodního chatu. \ + Více informací naleznete na Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Prosím uhraďte {0} pomocí zvolené platební metody prodejci XMR. V dalším kroku naleznete detaily o účtu prodejce.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -632,21 +719,35 @@ portfolio.pending.step2_buyer.sellersAddress={0} adresa prodejce portfolio.pending.step2_buyer.buyerAccount=Použijte svůj platební účet portfolio.pending.step2_buyer.paymentSent=Platba zahájena portfolio.pending.step2_buyer.warn=Platbu {0} jste ještě neprovedli!\nVezměte prosím na vědomí, že obchod musí být dokončen do {1}. -portfolio.pending.step2_buyer.openForDispute=Neukončili jste platbu!\nMax. doba obchodu uplynula. Obraťte se na mediátora a požádejte o pomoc. +portfolio.pending.step2_buyer.openForDispute=Neukončili jste platbu!\nMax. doba obchodu uplynula, ale platbu stále ještě můžete dokončit.\n\ + Obraťte se na mediátora a požádejte o pomoc. portfolio.pending.step2_buyer.paperReceipt.headline=Odeslali jste papírový doklad prodejci XMR? -portfolio.pending.step2_buyer.paperReceipt.msg=Zapamatujte si:\nMusíte napsat na papírový doklad: NO REFUNDS - bez náhrady.\nPoté ho roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. +portfolio.pending.step2_buyer.paperReceipt.msg=Zapamatujte si:\n\ + Musíte napsat na papírový doklad: NO REFUNDS - bez náhrady.\n\ + Poté ho roztrhněte na 2 části, vytvořte fotografii a odešlete ji na e-mailovou adresu prodejce XMR. portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline=Odeslat autorizační číslo a účtenku -portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Musíte zaslat autorizační číslo a fotografii dokladu e-mailem prodejci XMR.\nDoklad musí jasně uvádět celé jméno prodávajícího, zemi, stát a částku. E-mail prodejce je: {0}.\n\nOdeslali jste autorizační číslo a smlouvu prodejci? +portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg=Musíte zaslat autorizační číslo a fotografii dokladu e-mailem prodejci XMR.\n\ + Doklad musí jasně uvádět celé jméno prodávajícího, zemi, stát a částku. E-mail prodejce je: {0}.\n\n + Odeslali jste autorizační číslo a smlouvu prodejci? portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline=Pošlete MTCN a účtenku -portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Musíte odeslat MTCN (sledovací číslo) a fotografii dokladu e-mailem prodejci XMR.\nDoklad musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}.\n\nOdeslali jste MTCN a smlouvu prodejci? +portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg=Musíte odeslat MTCN (sledovací číslo) a fotografii dokladu e-mailem prodejci XMR.\n\ + Doklad musí jasně uvádět celé jméno prodávajícího, město, zemi a částku. E-mail prodejce je: {0}.\n\n + Odeslali jste MTCN a smlouvu prodejci? portfolio.pending.step2_buyer.halCashInfo.headline=Pošlete HalCash kód -portfolio.pending.step2_buyer.halCashInfo.msg=Musíte odeslat jak textovou zprávu s kódem HalCash tak i obchodní ID ({0}) prodejci XMR.\nMobilní číslo prodejce je {1}.\n\nPoslali jste kód prodejci? -portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Některé banky mohou ověřovat jméno příjemce. Účty Faster Payments vytvořené u starých klientů Haveno neposkytují jméno příjemce, proto si jej (v případě potřeby) vyžádejte pomocí obchodního chatu. +portfolio.pending.step2_buyer.halCashInfo.msg=Musíte odeslat jak textovou zprávu s kódem HalCash tak i \ + obchodní ID ({0}) prodejci XMR.\nMobilní číslo prodejce je {1}.\n\n + Poslali jste kód prodejci? +portfolio.pending.step2_buyer.fasterPaymentsHolderNameInfo=Některé banky mohou ověřovat jméno příjemce. \ + Účty Faster Payments vytvořené u starých klientů Haveno neposkytují jméno příjemce, \ + proto si jej (v případě potřeby) vyžádejte pomocí obchodního chatu. portfolio.pending.step2_buyer.confirmStart.headline=Potvrďte, že jste zahájili platbu portfolio.pending.step2_buyer.confirmStart.msg=Zahájili jste platbu {0} vašemu obchodnímu partnerovi? portfolio.pending.step2_buyer.confirmStart.yes=Ano, zahájil jsem platbu portfolio.pending.step2_buyer.confirmStart.proof.warningTitle=Neposkytli jste doklad o platbě -portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Nezadali jste ID transakce a klíč transakce.\n\nNeposkytnutím těchto údajů nemůže peer použít funkci automatického potvrzení k uvolnění XMR, jakmile bude přijat XMR.\nKromě toho Haveno vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\nDalší podrobnosti na wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. +portfolio.pending.step2_buyer.confirmStart.proof.noneProvided=Nezadali jste ID transakce a klíč transakce.\n\n\ + Neposkytnutím těchto údajů nemůže peer použít funkci automatického potvrzení k uvolnění XMR, jakmile bude přijat XMR.\n\ + Kromě toho Haveno vyžaduje, aby odesílatel transakce XMR mohl tyto informace poskytnout mediátorovi nebo rozhodci v případě sporu.\n\ + Další podrobnosti na wiki Haveno: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. portfolio.pending.step2_buyer.confirmStart.proof.invalidInput=Vstup není 32 bajtová hexadecimální hodnota portfolio.pending.step2_buyer.confirmStart.warningButton=Ignorovat a přesto pokračovat portfolio.pending.step2_seller.waitPayment.headline=Počkejte na platbu @@ -654,9 +755,20 @@ 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}'' 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.\nOdpovídat v chatu není povinné.\nPokud obchodník poruší některé z níže uvedených pravidel, zahajte spor a nahlaste jej mediátorovi nebo rozhodci.\n\nPravidla chatu:\n\t● Neposílejte žádné odkazy (riziko malwaru). Můžete odeslat ID transakce a jméno block exploreru.\n\t● Neposílejte seed slova, soukromé klíče, hesla nebo jiné citlivé informace!\n\t● Nepodporujte obchodování mimo Haveno (bez zabezpečení).\n\t● Nezapojujte se do žádných forem podvodů v oblasti sociálního inženýrství.\n\t● Pokud partner nereaguje a dává přednost nekomunikovat prostřednictvím chatu, respektujte jeho rozhodnutí.\n\t● Soustřeďte konverzaci pouze na obchod. Tento chat není náhradou messengeru.\n\t● Udržujte konverzaci přátelskou a uctivou. +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\ + Pokud obchodník poruší některé z níže uvedených pravidel, zahajte spor a nahlaste jej mediátorovi nebo rozhodci.\n\n\ + Pravidla chatu:\n\ + \t● Neposílejte žádné odkazy (riziko malwaru). Můžete odeslat ID transakce a jméno block exploreru.\n\ + \t● Neposílejte seed slova, soukromé klíče, hesla nebo jiné citlivé informace!\n\ + \t● Nepodporujte obchodování mimo Haveno (bez zabezpečení).\n\ + \t● Nezapojujte se do žádných forem podvodů v oblasti sociálního inženýrství.\n\ + \t● Pokud partner nereaguje a dává přednost nekomunikovat prostřednictvím chatu, respektujte jeho rozhodnutí.\n\ + \t● Soustřeďte konverzaci pouze na obchod. Tento chat není náhradou messengeru.\n\ + \t● Udržujte konverzaci přátelskou a uctivou. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Nedefinováno @@ -676,25 +788,43 @@ portfolio.pending.step3_buyer.wait.info=Čekání na potvrzení prodejce XMR na portfolio.pending.step3_buyer.wait.msgStateInfo.label=Stav zprávy o zahájení platby portfolio.pending.step3_buyer.warn.part1a=na {0} blockchainu portfolio.pending.step3_buyer.warn.part1b=u vašeho poskytovatele plateb (např. banky) -portfolio.pending.step3_buyer.warn.part2=Prodejce XMR vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda bylo odeslání platby úspěšné. -portfolio.pending.step3_buyer.openForDispute=Prodejce XMR nepotvrdil vaši platbu! Max. období pro uskutečnění obchodu uplynulo. Můžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. +portfolio.pending.step3_buyer.warn.part2=Prodejce XMR vaši platbu stále nepotvrdil. Zkontrolujte {0}, zda \ + bylo odeslání platby úspěšné. +portfolio.pending.step3_buyer.openForDispute=Prodejce XMR nepotvrdil vaši platbu! Max. období pro uskutečnění obchodu uplynulo. \ + Můžete počkat déle a dát obchodnímu partnerovi více času nebo požádat o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n -portfolio.pending.step3_seller.crypto.explorer=ve vašem oblíbeném {0} blockchain exploreru +portfolio.pending.step3_seller.crypto.explorer=ve vašem oblíbeném {0} průzkumníku blockchainu portfolio.pending.step3_seller.crypto.wallet=na vaší {0} peněžence -portfolio.pending.step3_seller.crypto={0}Zkontrolujte prosím {1}, zda transakce na vaši přijímací adresu\n{2}\nmá již dostatečné potvrzení na blockchainu.\nČástka platby musí být {3}\n\nPo zavření vyskakovacího okna můžete zkopírovat a vložit svou {4} adresu z hlavní obrazovky. +portfolio.pending.step3_seller.crypto={0}Zkontrolujte prosím {1}, zda transakce na vaši přijímací adresu\n\ +{2}\n\ +má již dostatečné potvrzení na blockchainu.\nČástka platby musí být {3}\n\n\ +Po zavření vyskakovacího okna můžete zkopírovat a vložit svou {4} adresu z hlavní obrazovky. portfolio.pending.step3_seller.postal={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"US Postal Money Order\". # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.payByMail={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"Pay by Mail\". +portfolio.pending.step3_seller.payByMail={0}Zkontrolujte, zda jste od kupujícího XMR obdrželi {1} přes \"Hotovost poštou\". # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step3_seller.bank=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\nPřejděte na webovou stránku online bankovnictví a zkontrolujte, zda jste od kupujícího XMR obdrželi {1}. -portfolio.pending.step3_seller.cash=Vzhledem k tomu, že se platba provádí prostřednictvím hotovostního vkladu, musí kupující XMR napsat na papírový doklad \"NO REFUND\", roztrhat ho na 2 části a odeslat vám e-mailem fotografii.\n\nAbyste se vyhnuli riziku zpětného zúčtování, potvrďte pouze, zda jste obdrželi e-mail a zda si jste jisti, že papírový doklad je platný.\nPokud si nejste jisti, {0} -portfolio.pending.step3_seller.moneyGram=Kupující vám musí zaslat e-mailem autorizační číslo a fotografii s potvrzením.\nPotvrzení musí jasně uvádět vaše celé jméno, zemi, stát a částku. Zkontrolujte si prosím váš e-mail, pokud jste obdrželi autorizační číslo.\n\nPo uzavření tohoto vyskakovacího okna se zobrazí jméno a adresa kupujícího XMR pro vyzvednutí peněz z MoneyGram.\n\nPotvrďte příjem až po úspěšném vyzvednutí peněz! -portfolio.pending.step3_seller.westernUnion=Kupující vám musí zaslat MTCN (sledovací číslo) a fotografii s potvrzením e-mailem.\nPotvrzení musí jasně uvádět vaše celé jméno, město, zemi a částku. Zkontrolujte svůj e-mail, pokud jste obdrželi MTCN.\n\nPo zavření tohoto vyskakovacího okna uvidíte jméno a adresu kupujícího XMR pro vyzvednutí peněz z Western Union.\n\nPotvrďte příjem až po úspěšném vyzvednutí peněz! -portfolio.pending.step3_seller.halCash=Kupující vám musí poslat kód HalCash jako textovou zprávu. Kromě toho obdržíte zprávu od HalCash s požadovanými informacemi pro výběr EUR z bankomatu podporujícího HalCash.\n\nPoté, co jste vyzvedli peníze z bankomatu, potvrďte zde přijetí platby! -portfolio.pending.step3_seller.amazonGiftCard=Kupující vám poslal e-mailovou kartu Amazon eGift e-mailem nebo textovou zprávou na váš mobilní telefon. Uplatněte nyní kartu Amazon eGift ve svém účtu Amazon a po přijetí potvrďte potvrzení o platbě. +portfolio.pending.step3_seller.bank=Váš obchodní partner potvrdil, že zahájil platbu {0}.\n\n\ + Přejděte na webovou stránku online bankovnictví a zkontrolujte, zda jste od kupujícího XMR obdrželi {1}. +portfolio.pending.step3_seller.cash=Vzhledem k tomu, že se platba provádí prostřednictvím hotovostního vkladu, musí kupující XMR napsat na papírový doklad \"NO REFUND\", roztrhat ho na 2 části a odeslat vám e-mailem fotografii.\n\n\ +Abyste se vyhnuli riziku zpětného zúčtování, potvrďte pouze, zda jste obdrželi e-mail a zda si jste jisti, že papírový doklad je platný.\n\ +Pokud si nejste jisti, {0} +portfolio.pending.step3_seller.moneyGram=Kupující vám musí zaslat e-mailem autorizační číslo a fotografii s potvrzením.\n\ + Potvrzení musí jasně uvádět vaše celé jméno, zemi, stát a částku. Zkontrolujte si prosím váš e-mail, pokud jste obdrželi autorizační číslo.\n\n\ + Po uzavření tohoto vyskakovacího okna se zobrazí jméno a adresa kupujícího XMR pro vyzvednutí peněz z MoneyGram.\n\n\ + Potvrďte příjem až po úspěšném vyzvednutí peněz! +portfolio.pending.step3_seller.westernUnion=Kupující vám musí zaslat MTCN (sledovací číslo) a fotografii s potvrzením e-mailem.\n\ + Potvrzení musí jasně uvádět vaše celé jméno, město, zemi a částku. Zkontrolujte svůj e-mail, pokud jste obdrželi MTCN.\n\n\ + Po zavření tohoto vyskakovacího okna uvidíte jméno a adresu kupujícího XMR pro vyzvednutí peněz z Western Union.\n\n\ + Potvrďte příjem až po úspěšném vyzvednutí peněz! +portfolio.pending.step3_seller.halCash=Kupující vám musí poslat kód HalCash jako textovou zprávu. Kromě toho obdržíte zprávu od HalCash s požadovanými informacemi pro výběr EUR z bankomatu podporujícího HalCash.\n\n\ + Poté, co jste vyzvedli peníze z bankomatu, potvrďte zde přijetí platby! +portfolio.pending.step3_seller.amazonGiftCard=Kupující vám poslal e-mailovou kartu Amazon eGift e-mailem nebo textovou zprávou \ + na váš mobilní telefon. Uplatněte nyní kartu Amazon eGift ve svém účtu Amazon \ + a po přijetí potvrďte potvrzení o platbě. -portfolio.pending.step3_seller.bankCheck=\n\nOvěřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje s jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\nPokud jména nejsou úplně stejná, {1} +portfolio.pending.step3_seller.bankCheck=\n\nOvěřte také, zda se jméno odesílatele uvedené v obchodní smlouvě shoduje s jménem uvedeným na výpisu z účtu:\nJméno odesílatele podle obchodní smlouvy: {0}\n\n\ + Pokud jména nejsou úplně stejná, {1} # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.openDispute=nepotvrzujte příjem platby. Místo toho otevřete spor stisknutím \"alt + o\" nebo \"option + o\".\n\n portfolio.pending.step3_seller.confirmPaymentReceipt=Potvrďte příjem platby @@ -711,8 +841,10 @@ portfolio.pending.step3_seller.buyerStartedPayment.crypto=Podívejte se na potvr portfolio.pending.step3_seller.buyerStartedPayment.traditional=Zkontrolujte na svém obchodním účtu (např. Bankovní účet) a potvrďte, kdy jste platbu obdrželi. portfolio.pending.step3_seller.warn.part1a=na {0} blockchainu portfolio.pending.step3_seller.warn.part1b=u vašeho poskytovatele plateb (např. banky) -portfolio.pending.step3_seller.warn.part2=Stále jste nepotvrdili přijetí platby. Zkontrolujte {0}, zda jste obdrželi platbu. -portfolio.pending.step3_seller.openForDispute=Nepotvrdili jste příjem platby!\nUplynulo max. období obchodu.\nPotvrďte nebo požádejte o pomoc mediátora. +portfolio.pending.step3_seller.warn.part2=Stále jste nepotvrdili přijetí platby. \ + Zkontrolujte {0}, zda jste obdrželi platbu. +portfolio.pending.step3_seller.openForDispute=Nepotvrdili jste příjem platby!\n\ + Uplynulo max. období obchodu.\nPotvrďte nebo požádejte o pomoc mediátora. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Obdrželi jste od svého obchodního partnera platbu v měně {0}?\n\n # suppress inspection "TrailingSpacesInProperty" @@ -721,23 +853,20 @@ portfolio.pending.step3_seller.onPaymentReceived.name=Ověřte také, zda se jm portfolio.pending.step3_seller.onPaymentReceived.note=Vezměte prosím na vědomí, že jakmile potvrdíte příjem, dosud uzamčený obchodovaný XMR bude uvolněn kupujícímu a kauce bude vrácena.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Potvrďte, že jste obdržel(a) platbu portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Ano, obdržel(a) jsem platbu -portfolio.pending.step3_seller.onPaymentReceived.signer=DŮLEŽITÉ: Potvrzením přijetí platby ověřujete také účet protistrany a odpovídajícím způsobem jej podepisujete. Protože účet protistrany dosud nebyl podepsán, měli byste odložit potvrzení platby co nejdéle, abyste snížili riziko zpětného zúčtování. +portfolio.pending.step3_seller.onPaymentReceived.signer=DŮLEŽITÉ: Potvrzením přijetí platby ověřujete také \ + účet protistrany a odpovídajícím způsobem jej podepisujete. Protože účet protistrany dosud nebyl podepsán, \ + měli byste odložit potvrzení platby co nejdéle, abyste snížili riziko zpětného zúčtování. portfolio.pending.step5_buyer.groupTitle=Shrnutí dokončeného obchodu +portfolio.pending.step5_buyer.groupTitle.mediated=Tento obchod byl vyřešen pomocí mediátora +portfolio.pending.step5_buyer.groupTitle.arbitrated=Tento obchod byl vyřešen pomocí rozhodce portfolio.pending.step5_buyer.tradeFee=Obchodní poplatek portfolio.pending.step5_buyer.makersMiningFee=Poplatek za těžbu portfolio.pending.step5_buyer.takersMiningFee=Celkové poplatky za těžbu portfolio.pending.step5_buyer.refunded=Vrácená kauce -portfolio.pending.step5_buyer.withdrawXMR=Vyberte své moneroy -portfolio.pending.step5_buyer.amount=Částka k výběru -portfolio.pending.step5_buyer.withdrawToAddress=Adresa výběru -portfolio.pending.step5_buyer.moveToHavenoWallet=Uchovat prostředky v peněžence Haveno -portfolio.pending.step5_buyer.withdrawExternal=Vybrat do externí peněženky -portfolio.pending.step5_buyer.alreadyWithdrawn=Vaše finanční prostředky již byly vybrány.\nZkontrolujte historii transakcí. -portfolio.pending.step5_buyer.confirmWithdrawal=Potvrďte žádost o výběr portfolio.pending.step5_buyer.amountTooLow=Částka k převodu je nižší než transakční poplatek a min. možná hodnota tx (drobné). -portfolio.pending.step5_buyer.withdrawalCompleted.headline=Výběr byl dokončen -portfolio.pending.step5_buyer.withdrawalCompleted.msg=Vaše dokončené obchody jsou uloženy na \"Portfolio/Historie\".\nVšechny své moneroové transakce si můžete prohlédnout v sekci \"Prostředky/Transakce\" +portfolio.pending.step5_buyer.tradeCompleted.headline=Obchod dokončen +portfolio.pending.step5_buyer.tradeCompleted.msg=Vaše dokončené obchody jsou uchovávány pod \"Portfolio/Historie\".\nVšechny své monero transakce najdete pod \"Prostředky/Transakce\" portfolio.pending.step5_buyer.bought=Koupili jste portfolio.pending.step5_buyer.paid=Zaplatili jste @@ -757,25 +886,35 @@ portfolio.pending.tradePeriodInfo=Po prvním potvrzení na blockchainu začíná portfolio.pending.tradePeriodWarning=Pokud je tato lhůta překročena, mohou oba obchodníci zahájit spor. portfolio.pending.tradeNotCompleted=Obchod nebyl dokončen včas (do {0}) portfolio.pending.tradeProcess=Obchodní proces +portfolio.pending.stillNotResolved=Pokud váš problém zůstává nevyřešen, můžete požádat o podporu v naší [Matrix místnosti](https://matrix.to/#/#haveno:monero.social). + portfolio.pending.openAgainDispute.msg=Pokud si nejste jisti, že zpráva pro mediátora nebo rozhodce dorazila (např. Pokud jste nedostali odpověď po 1 dni), neváhejte znovu zahájit spor s Cmd/Ctrl+o. Můžete také požádat o další pomoc na fóru Haveno na adrese [HYPERLINK:https://haveno.community]. portfolio.pending.openAgainDispute.button=Otevřete spor znovu portfolio.pending.openSupportTicket.headline=Otevřít úkol pro podporu -portfolio.pending.openSupportTicket.msg=Použijte tuto funkci pouze v naléhavých případech, pokud nevidíte tlačítko \"Otevřít podporu\" nebo \"Otevřít spor\".\n\nKdyž otevřete dotaz podporu, obchod bude přerušen a zpracován mediátorem nebo rozhodcem. +portfolio.pending.openSupportTicket.msg=Použijte tuto funkci pouze v naléhavých případech, \ + pokud nevidíte tlačítko \"Otevřít podporu\" nebo \"Otevřít spor\".\n\nKdyž otevřete úkol pro podporu, obchod bude přerušen \ + a zpracován mediátorem nebo rozhodcem. portfolio.pending.timeLockNotOver=Než budete moci zahájit rozhodčí spor, musíte počkat do ≈{0} ({1} dalších bloků). -portfolio.pending.error.depositTxNull=Vkladová operace je nulová. Nemůžete otevřít spor bez platné vkladové transakce. Přejděte do \"Nastavení/Informace o síti\" a proveďte resynchronizaci SPV.\n\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -portfolio.pending.mediationResult.error.depositTxNull=Vkladová transakce je nulová. Obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.mediationResult.error.delayedPayoutTxNull=Odložená výplatní transakce je nulová. Obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.error.depositTxNotConfirmed=Vkladová transakce není potvrzena. Nemůžete zahájit rozhodčí spor s nepotvrzenou vkladovou transakcí. Počkejte prosím, až bude potvrzena, nebo přejděte do \"Nastavení/Informace o síti\" a proveďte resynchronizaci SPV.\n\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +portfolio.pending.error.depositTxNull=Vkladová operace je nulová. Nemůžete otevřít spor v případě \ + neplatné vkladové transakce.\n\n\ + Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. +portfolio.pending.mediationResult.error.depositTxNull=Vkladová transakce je nulová. Obchod můžete přesunout \ + do neúspěšných obchodů. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=Zpožděný výplatní transakce je nulová. Obchod můžete přesunout \ + do neúspěšných obchodů. +portfolio.pending.error.depositTxNotConfirmed=Vkladová transakce není potvrzena. Nemůžete zahájit rozhodčí spor s nepotvrzenou vkladovou transakcí. \ + Počkejte prosím, až bude potvrzena a dostupná.\n\n\ + Pro další pomoc kontaktujte podporu Haveno pomocí naší místnosti na Matrixu. portfolio.pending.support.headline.getHelp=Potřebujete pomoc? -portfolio.pending.support.text.getHelp=Pokud máte nějaké problémy, můžete zkusit kontaktovat obchodníka v obchodním chatu nebo požádat komunitu Haveno na adrese https://haveno.community. Pokud váš problém stále není vyřešen, můžete požádat mediátora o další 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.mediationRequested=Mediace požádána -portfolio.pending.refundRequested=Požadováno vrácení peněz +portfolio.pending.arbitrationRequested=Požádáno o arbitráž +portfolio.pending.mediationRequested=Požádáno o mediaci +portfolio.pending.refundRequested=Požádáno o vrácení peněz portfolio.pending.openSupport=Otevřít úkol pro podporu portfolio.pending.supportTicketOpened=Úkol pro podporu otevřen portfolio.pending.communicateWithArbitrator=Komunikujte prosím na obrazovce \"Podpora\" s rozhodcem. @@ -791,24 +930,84 @@ portfolio.pending.mediationResult.info.peerAccepted=Váš obchodní partner při portfolio.pending.mediationResult.button=Zobrazit navrhované řešení portfolio.pending.mediationResult.popup.headline=Výsledek mediace obchodu s ID: {0} portfolio.pending.mediationResult.popup.headline.peerAccepted=Váš obchodní partner přijal návrh mediátora na obchod {0} -portfolio.pending.mediationResult.popup.info=Mediátor navrhl následující výplatu:\nObdržíte: {0}\nVáš obchodní partner obdrží: {1}\n\nTuto navrhovanou výplatu můžete přijmout nebo odmítnout.\n\nPřijetím podepíšete navrhovanou výplatní transakci. Pokud váš obchodní partner také přijme a podepíše, výplata bude dokončena a obchod bude uzavřen.\n\nPokud jeden nebo oba odmítnete návrh, budete muset počkat do {2} (blok {3}), abyste zahájili spor druhého kola s rozhodcem, který případ znovu prošetří a na základě svých zjištění provede výplatu.\n\nRozhodce může jako náhradu za svou práci účtovat malý poplatek (maximální poplatek: bezpečnostní záloha obchodníka). Oba obchodníci, kteří souhlasí s návrhem zprostředkovatele, jsou na dobré cestě - žádost o arbitráž je určena pro výjimečné okolnosti, například pokud je obchodník přesvědčen, že zprostředkovatel neučinil návrh na spravedlivou výplatu (nebo pokud druhý partner nereaguje).\n\nDalší podrobnosti o novém rozhodčím modelu: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] -portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Přijali jste výplatu navrženou mediátorem, ale zdá se, že váš obchodní partner ji nepřijal.\n\nPo uplynutí doby uzamčení na {0} (blok {1}) můžete zahájit spor druhého kola s rozhodcem, který případ znovu prošetří a na základě jeho zjištění provede platbu.\n\nDalší podrobnosti o rozhodčím modelu najdete na adrese: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#arbitration] +portfolio.pending.mediationResult.popup.info=Mediátor navrhl následující výplatu:\n\ + Obdržíte: {0}\n\ + Váš obchodní partner obdrží: {1}\n\n\ + Tuto navrhovanou výplatu můžete přijmout nebo odmítnout.\n\n\ + Přijetím podepíšete navrhovanou výplatní transakci. \ + Pokud váš obchodní partner také přijme a podepíše, výplata bude dokončena a obchod bude uzavřen.\n\n\ + Pokud jeden nebo oba odmítnete návrh, budete muset počkat do {2} (blok {3}), abyste zahájili spor \ + druhého kola s rozhodcem, který případ znovu prošetří a na základě svých zjištění provede výplatu.\n\n\ + Rozhodce může jako náhradu za svou práci účtovat malý poplatek (maximální poplatek: bezpečnostní záloha obchodníka). \ + Oba obchodníci, kteří souhlasí s návrhem zprostředkovatele, jsou na dobré cestě - žádost o arbitráž je určena pro \ + výjimečné okolnosti, například pokud je obchodník přesvědčen, že zprostředkovatel neučinil návrh na spravedlivou výplatu \ + (nebo pokud druhý partner nereaguje).\n\n\ + Další podrobnosti o novém rozhodčím modelu: [HYPERLINK:https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=Přijali jste výplatu navrženou mediátorem, \ + ale zdá se, že váš obchodní partner ji nepřijal.\n\n\ + Po uplynutí doby uzamčení na {0} (blok {1}) můžete zahájit spor druhého kola s rozhodcem, který případ \ + znovu prošetří a na základě jeho zjištění provede platbu.\n\n\ + Další podrobnosti o rozhodčím modelu najdete na adrese:\ + [https://haveno.exchange/wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.openArbitration=Odmítnout a požádat o arbitráž portfolio.pending.mediationResult.popup.alreadyAccepted=Už jste přijali -portfolio.pending.failedTrade.taker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\nBez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky a nebyl zaplacen žádný obchodní poplatek. Tento obchod můžete přesunout do neúspěšných obchodů. -portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\nBez 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\nBez tohoto 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/bisq-network/support/issues]\n\nKlidně můžete přesunout tento obchod do neúspěšných obchodů. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Odložená výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\nNezasílejte prosím fiat nebo crypto platbu prodejci XMR, protože bez odložené platby tx nelze zahájit arbitráž. Místo toho otevřete mediační úkol pomocí Cmd/Ctrl+o. Mediátor by měl navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\nO vrácení ztracených obchodních poplatků můžete požádat zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Odložená výplatní transakce chybí, ale prostředky byly v depozitní transakci uzamčeny.\n\nPokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel mediační úkol. Měli byste také otevřít mediační úkol pomocí Cmd/Ctrl+o.\n\nPokud kupující ještě neposlal platbu, měl by zprostředkovatel navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). Jinak by částka obchodu měla jít kupujícímu.\n\nO vrácení ztracených obchodních poplatků můžete požádat zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\nChyba: {0}\n\nJe možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol a získejte radu od mediátorů Haveno.\n\nPokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. O vrácení ztracených obchodních poplatků požádejte zde: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\nObchod nelze dokončit a možná jste ztratili poplatek za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.taker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ + Bez tohoto tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky a nebyl zaplacen žádný obchodní poplatek. \ + Tento obchod můžete přesunout do neúspěšných obchodů. +portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce transakce.\n\n\ + 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.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áž. \ + Místo toho otevřete mediační úkol pomocí Cmd/Ctrl+o. \ + Mediátor by měl navrhnout, aby oba partneři dostali zpět celou částku svých bezpečnostních vkladů \ + (přičemž prodejce také obdrží plnou částku obchodu). \ + Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\n\ + O vrácení ztracených obchodních poplatků můžete požádat zde: \ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, \ + ale prostředky byly v depozitní transakci uzamčeny.\n\n\ + Pokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel \ + mediační úkol. Měli byste také otevřít mediační úkol pomocí Cmd/Ctrl+o.\n\n\ + Pokud kupující ještě neposlal platbu, měl by mediátor navrhnout, aby oba partneři dostali zpět celou částku \ + svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). \ + Jinak by částka obchodu měla jít kupujícímu.\n\n\ + O vrácení ztracených obchodních poplatků můžete požádat zde: \ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\n + Chyba: {0}\n\n\ + Je možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol \ + a získejte radu od mediátorů Haveno.\n\n\ + Pokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. \ + O vrácení ztracených obchodních poplatků požádejte zde: \ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\n\ +Obchod nelze dokončit a možná jste ztratili poplatek \ + za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: \ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Obchodní protokol narazil na některé problémy.\n\n{0} -portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\nChcete obchod přesunout do neúspěšných obchodů?\n\nZ obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět na obrazovku otevřených obchodů. -portfolio.pending.failedTrade.txChainValid.moveToFailed=Obchodní protokol narazil na některé problémy.\n\n{0}\n\nObchodní transakce byly zveřejněny a finanční prostředky jsou uzamčeny. Přesuňte obchod do neúspěšných obchodů, pouze pokud jste si opravdu jisti. Může to bránit možnostem řešení problému.\n\nChcete obchod přesunout do neúspěšných obchodů?\n\nZ obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět na obrazovku otevřených obchodů. +portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\n\ + Chcete obchod přesunout do neúspěšných obchodů?\n\n\ +Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod zpět \ + na obrazovku otevřených obchodů. +portfolio.pending.failedTrade.txChainValid.moveToFailed=Obchodní protokol narazil na některé problémy.\n\n{0}\n\n\ + Obchodní transakce byly zveřejněny a finanční prostředky jsou uzamčeny. Přesuňte obchod do neúspěšných obchodů, \ + pouze pokud jste si opravdu jisti. Může to bránit možnostem řešení problému.\n\n\ + Chcete obchod přesunout do neúspěšných obchodů?\n\n\ + Z obrazovky neúspěšných obchodů nemůžete otevřít mediaci nebo arbitráž, ale můžete kdykoli přesunout neúspěšný obchod \ + zpět na obrazovku otevřených obchodů. portfolio.pending.failedTrade.moveTradeToFailedIcon.tooltip=Přesuňte obchod do neúspěšných obchodů portfolio.pending.failedTrade.warningIcon.tooltip=Kliknutím otevřete podrobnosti o problémech tohoto obchodu portfolio.failed.revertToPending.popup=Chcete přesunout tento obchod do otevřených obchodů? +portfolio.failed.revertToPending.failed=Selhal přesun tohoto obchodu do otevřených obchodů. portfolio.failed.revertToPending=Přesunout obchod do otevřených obchodů portfolio.closed.completed=Dokončeno @@ -816,11 +1015,18 @@ portfolio.closed.ticketClosed=Rozhodnuto portfolio.closed.mediationTicketClosed=Mediováno portfolio.closed.canceled=Zrušeno portfolio.failed.Failed=Selhalo -portfolio.failed.unfail=Před pokračováním se ujistěte, že máte zálohu vašeho datového adresáře!\nChcete tento obchod přesunout zpět do otevřených obchodů?\nJe to způsob, jak odemknout finanční prostředky uvízlé v neúspěšném obchodu. -portfolio.failed.cantUnfail=Tento obchod nelze v tuto chvíli přesunout zpět do otevřených obchodů.\nZkuste to znovu po dokončení obchodu (obchodů) {0} +portfolio.failed.unfail=Před pokračováním se ujistěte, že máte zálohu vašeho datového adresáře!\n\ + Chcete tento obchod přesunout zpět do otevřených obchodů?\n\ + Je to způsob, jak odemknout finanční prostředky uvízlé v neúspěšném obchodu. +portfolio.failed.cantUnfail=Tento obchod nelze v tuto chvíli přesunout zpět do otevřených obchodů.\n\ + Zkuste to znovu po dokončení obchodu (obchodů) {0} portfolio.failed.depositTxNull=Obchod nelze změnit zpět na otevřený obchod. Transakce s vkladem je neplatná. -portfolio.failed.delayedPayoutTxNull=Obchod nelze změnit zpět na otevřený obchod. Odložená výplatní transakce je nulová. - +portfolio.failed.delayedPayoutTxNull=Obchod nelze změnit zpět na otevřený obchod. Zpožděná výplatní transakce je nulová. +portfolio.failed.penalty.msg=Toto strhne {0}/{1} poplatek penále {2} a vrátí zbytek obchodovaných financí do jejich peněženky. Jste si jisti, že chcete odeslat?\n\n\ + Jiné info:\n\ + Transakční poplatek: {3}\n\ + Rezervní hash Tx: {4} +portfolio.failed.error.msg=Záznam obchodu neexistuje. #################################################################### # Funds @@ -834,9 +1040,12 @@ funds.tab.transactions=Transakce funds.deposit.unused=Nepoužito funds.deposit.usedInTx=Používá se v {0} transakcích +funds.deposit.baseAddress=Základní adresa +funds.deposit.offerFunding=Rezervováno pro financování nabídky ({0}) +funds.deposit.tradePayout=Rezervováno pro výplatu obchodu ({0}) funds.deposit.fundHavenoWallet=Financovat Haveno peněženku funds.deposit.noAddresses=Dosud nebyly vygenerovány žádné adresy pro vklad -funds.deposit.fundWallet=Financujte svou peněženku +funds.deposit.fundWallet=Financovat peněženku funds.deposit.withdrawFromWallet=Pošlete peníze z peněženky funds.deposit.amount=Částka v XMR (volitelná) funds.deposit.generateAddress=Vygenerujte novou adresu @@ -848,11 +1057,13 @@ funds.withdrawal.inputs=Volba vstupů funds.withdrawal.useAllInputs=Použijte všechny dostupné vstupy funds.withdrawal.useCustomInputs=Použijte vlastní vstupy funds.withdrawal.receiverAmount=Částka pro příjemce +funds.withdrawal.sendMax=Poslat max. dostupné funds.withdrawal.senderAmount=Náklad pro odesílatele funds.withdrawal.feeExcluded=Částka nezahrnuje poplatek za těžbu funds.withdrawal.feeIncluded=Částka zahrnuje poplatek za těžbu funds.withdrawal.fromLabel=Výběr z adresy funds.withdrawal.toLabel=Adresa příjemce +funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Poznámka k výběru funds.withdrawal.memo=Volitelně vyplňte poznámku funds.withdrawal.withdrawButton=Odeslat výběr @@ -865,7 +1076,14 @@ funds.withdrawal.selectAddress=Vyberte zdrojovou adresu z tabulky funds.withdrawal.setAmount=Nastavte částku k výběru funds.withdrawal.fillDestAddress=Vyplňte svou cílovou adresu funds.withdrawal.warn.noSourceAddressSelected=Ve výše uvedené tabulce musíte vybrat zdrojovou adresu. -funds.withdrawal.warn.amountExceeds=Na vybrané adrese nemáte dostatek prostředků.\nZvažte výběr více adres ve výše uvedené tabulce nebo změňte přepínač poplatků tak, aby zahrnoval poplatek za těžbu. +funds.withdrawal.warn.amountExceeds=Na vybrané adrese nemáte dostatek prostředků.\n\ + Zvažte výběr více adres ve výše uvedené tabulce nebo změňte přepínač poplatků tak, aby zahrnoval poplatek za těžbu. +funds.withdrawal.warn.amountMissing=Zadejte částku k vybrání +funds.withdrawal.txFee=Poplatek transakce výběru (satoshi/vbyte) +funds.withdrawal.useCustomFeeValueInfo=Zadejte vlastní hodnotu poplatku transakce +funds.withdrawal.useCustomFeeValue=Použít vlastní hodnotu +funds.withdrawal.txFeeMin=Poplatek transakce musí být alespoň {0} satoshi/vbyte +funds.withdrawal.txFeeTooLarge=Vámi zadaná hodnota je nad rozumnou hodnotou (>5000 satoshi/vbyte). Poplatek transakce je obvykle v rozmezí 50-400 satoshi/vbyte. funds.reserved.noFunds=V otevřených nabídkách nejsou rezervovány žádné finanční prostředky funds.reserved.reserved=Rezervováno v místní peněžence pro nabídku s ID: {0} @@ -895,18 +1113,36 @@ funds.tx.revert=Vrátit funds.tx.txSent=Transakce byla úspěšně odeslána na novou adresu v lokální peněžence Haveno. funds.tx.direction.self=Posláno sobě funds.tx.dustAttackTx=Přijaté drobné -funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku XMR a může se jednat o pokus společností provádějících analýzu blockchainu o špehování vaší peněženky.\n\nPoužijete-li tento transakční výstup ve výdajové transakci, zjistí, že jste pravděpodobně také vlastníkem jiné adresy (sloučení mincí).\n\nKvůli ochraně vašeho soukromí ignoruje peněženka Haveno takové drobné výstupy pro účely utrácení a na obrazovce zůstatku. Můžete nastavit hodnotu "drobnosti", kdy je výstup považován za drobné, v nastavení. +funds.tx.dustAttackTx.popup=Tato transakce odesílá do vaší peněženky velmi malou částku XMR a může se jednat o pokus \ + společností provádějících analýzu blockchainu o špehování vaší peněženky.\n\n\ + Použijete-li tento transakční výstup ve výdajové transakci, zjistí, že jste pravděpodobně také vlastníkem \ + jiné adresy (sloučení mincí).\n\n\ + Kvůli ochraně vašeho soukromí ignoruje peněženka Haveno takové drobné výstupy pro účely utrácení a na obrazovce zůstatku. \ + V nastavení můžete nastavit prahovou hodnotu, při které je výstup považován za drobné(dust). #################################################################### # Support #################################################################### support.tab.mediation.support=Mediace +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\ + Poplatek penále tvůrce: {1}\n\ + Rezervní poplatek těžby Tx: {2}\n\ + Rezervní hash Tx: {3}\n\ + Rezervní klíčové obrázky Tx: {4}\n\ + +support.contextmenu.penalize.msg=Penalizovat {0} zveřejněním tx rezervy +support.prompt.signedOffer.error.msg=Podepsaný záznam nabídky neexistuje; kontaktujte správce. +support.info.submitTxHex=Rezervní transakce byla zveřejněna s tímto výsledkem:\n +support.result.success=Transakce hex byla úspěšně zadána. support.sigCheck.button=Ověřit podpis support.sigCheck.popup.info=Vložte souhrnnou zprávu procesu zprostředkování. S tímto nástrojem může každý uživatel zkontrolovat, zda se podpis zprostředkovatele shoduje se souhrnnou zprávou. @@ -919,6 +1155,7 @@ support.sigCheck.popup.failed=Ověření podpisu selhalo support.sigCheck.popup.invalidFormat=Zpráva nemá očekávaný formát. Zkopírujte a vložte souhrnnou zprávu ze sporu. support.reOpenByTrader.prompt=Opravdu chcete spor znovu otevřít? +support.reOpenByTrader.failed=Opětovné otevření sporu selhalo. support.reOpenButton.label=Znovu otevřít support.sendNotificationButton.label=Soukromé oznámení support.reportButton.label=Zpráva @@ -928,7 +1165,9 @@ support.sendingMessage=Odesílání zprávy... support.receiverNotOnline=Příjemce není online. Zpráva je uložena v jejich schránce. support.sendMessageError=Odeslání zprávy se nezdařilo. Chyba: {0} support.receiverNotKnown=Příjemce není znám -support.wrongVersion=Nabídka v tomto sporu byla vytvořena se starší verzí Haveno.\nTento spor nemůžete ukončit s touto verzí aplikace.\n\nPoužijte prosím starší verzi s verzí protokolu {0} +support.wrongVersion=Nabídka v tomto sporu byla vytvořena se starší verzí Haveno.\n\ +Tento spor nemůžete ukončit s touto verzí aplikace.\n\n\ +Použijte prosím starší verzi s verzí protokolu {0} support.openFile=Otevřete soubor, který chcete připojit (maximální velikost souboru: {0} kb) support.attachmentTooLarge=Celková velikost vašich příloh je {0} kb a překračuje maximální povolenou velikost zprávy {1} kB. support.maxSize=Max. povolená velikost souboru je {0} kB. @@ -943,24 +1182,71 @@ support.closeTicket=Zavřít úkol support.attachments=Přílohy: support.savedInMailbox=Zpráva uložena ve schránce příjemce support.arrived=Zpráva dorazila k příjemci +support.transient=Zpráva je na cestě k příjemci support.acknowledged=Přijetí zprávy potvrzeno příjemcem support.error=Příjemce nemohl zpracovat zprávu. Chyba: {0} +support.errorTimeout=vypršení platnosti. Zkuste zprávu odeslat znovu. support.buyerAddress=Adresa kupujícího XMR support.sellerAddress=Adresa prodejce XMR support.role=Role support.agent=Agent podpory support.state=Stav support.chat=Chat +support.requested=Požádáno support.closed=Zavřeno -support.open=Otevřené +support.open=Otevřeno +support.moreButton=VÍCE... +support.sendLogFiles=Odeslat soubory logů +support.uploadTraderChat=Nahrát obchodní chat support.process=Rozhodnout support.buyerMaker=Kupující XMR/Tvůrce support.sellerMaker=Prodejce XMR/Tvůrce support.buyerTaker=Kupující XMR/Příjemce support.sellerTaker=Prodávající XMR/Příjemce +support.sendLogs.title=Odeslat logy +support.sendLogs.backgroundInfo=Pokud se vyskytne chyba, rozhodčí a pracovníci podpory si často vyžádají kopie souborů log, aby mohli problém prozkoumat.\n\n\ + Po stisku 'Odeslat' dojde ke kompresi a odeslání logů přímo rozhodci. +support.sendLogs.step1=Vytvořit archiv Zip s log soubory +support.sendLogs.step2=Požadavek připojení k rozhodci +support.sendLogs.step3=Nahrát archivovaná data logů +support.sendLogs.send=Odeslat +support.sendLogs.cancel=Zrušit +support.sendLogs.init=Zavádění +support.sendLogs.retry=Opakování odeslání +support.sendLogs.stopped=Přenos zastaven +support.sendLogs.progress=Průběh přenosu: %.0f%% +support.sendLogs.finished=Přenos dokončen! +support.sendLogs.command=Stiskněte 'Odeslat' pro opakování, nebo 'Zastavit' pro zrušení +support.txKeyImages=Klíčové obrázky +support.txHash=Hash transakce +support.txHex=Hex transakce +support.signature=Podpis +support.maker.penalty.fee=Poplatek penále tvůrce +support.tx.miner.fee=Poplatek těžby -support.backgroundInfo=Haveno není společnost, takže spory řeší jinak.\n\nObchodníci mohou v rámci aplikace komunikovat prostřednictvím zabezpečeného chatu na obrazovce otevřených obchodů a pokusit se o řešení sporů sami. Pokud to nestačí, arbitr rozhodne o situaci a určí výplatu obchodních prostředků. -support.initialInfo=Do níže uvedeného textového pole zadejte popis problému. Přidejte co nejvíce informací k urychlení doby řešení sporu.\n\nZde je kontrolní seznam informací, které byste měli poskytnout:\n\t● Pokud kupujete XMR: Provedli jste převod Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko „Platba zahájena“?\n\t● Pokud jste prodejcem XMR: Obdrželi jste platbu Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko „Platba přijata“?\n\t● Kterou verzi Haveno používáte?\n\t● Jaký operační systém používáte?\n\t● Pokud se vyskytl problém s neúspěšnými transakcemi, zvažte přechod na nový datový adresář.\n\t Někdy dojde k poškození datového adresáře a vede to k podivným chybám.\n\t  Viz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\nSeznamte se prosím se základními pravidly procesu sporu:\n\t● Musíte odpovědět na požadavky {0} do 2 dnů.\n\t● Mediátoři reagují do 2 dnů. Rozhodci odpoví do 5 pracovních dnů.\n\t● Maximální doba sporu je 14 dní.\n\t● Musíte spolupracovat s {1} a poskytnout informace, které požaduje, aby jste vyřešili váš případ.\n\t● Při prvním spuštění aplikace jste přijali pravidla uvedena v dokumentu sporu v uživatelské smlouvě.\n\nDalší informace o procesu sporu naleznete na: {2} +support.backgroundInfo=Haveno není společnost, takže spory řeší jinak.\n\n\ +Obchodníci mohou v rámci aplikace komunikovat prostřednictvím zabezpečeného chatu na obrazovce otevřených obchodů \ + a sami se pokusit o řešení sporů. Pokud to nestačí, arbitr rozhodne o situaci \ + a určí výplatu obchodních prostředků. +support.initialInfo=Do níže uvedeného textového pole zadejte popis problému. \ + Přidejte co nejvíce informací k urychlení doby řešení sporu.\n\n\ + Zde je kontrolní seznam informací, které byste měli poskytnout:\n\ + \● Pokud kupujete XMR: Provedli jste převod Fiat nebo Cryptou? Pokud ano, klikli jste v aplikaci na tlačítko 'Platba zahájena'?\n\ + \t● Pokud jste prodejcem XMR: Obdrželi jste platbu fiat nebo kryptoměny? Pokud ano, \ +klikli jste v aplikaci \ + na tlačítko 'Platba přijata'?\n\ + \t● Kterou verzi Haveno používáte?\n\ + \t● Jaký operační systém používáte?\n\ + \t● Pokud se vyskytl problém s neúspěšnými transakcemi, zvažte přechod na nový datový adresář.\n\ + \t Někdy dojde k poškození datového adresáře a vede to k podivným chybám.\n\ + \t  Viz: https://docs.haveno.exchange/backup-recovery.html#switch-to-a-new-data-directory\n\n\ + Seznamte se prosím se základními pravidly procesu sporu:\n\ +\t● Musíte odpovědět na požadavky {0} do 2 dnů.\n\ +\t● Mediátoři reagují do 2 dnů. Rozhodci odpoví do 5 pracovních dnů.\n\ +\t● Maximální doba sporu je 14 dní.\n\ +\t● Musíte spolupracovat s {1} a poskytnout informace, které požaduje, aby jste vyřešili váš případ.\n\ +\t● Při prvním spuštění aplikace jste přijali pravidla uvedena v dokumentu sporu v uživatelské smlouvě.\n\n\ +Další informace o procesu sporu naleznete na: {2} support.systemMsg=Systémová zpráva: {0} support.youOpenedTicket=Otevřeli jste žádost o podporu.\n\n{0}\n\nVerze Haveno: {1} support.youOpenedDispute=Otevřeli jste žádost o spor.\n\n{0}\n\nVerze Haveno: {1} @@ -969,8 +1255,15 @@ support.peerOpenedTicket=Váš obchodní partner požádal o podporu kvůli tech support.peerOpenedDispute=Váš obchodní partner požádal o spor.\n\n{0}\n\nHaveno verze: {1} support.peerOpenedDisputeForMediation=Váš obchodní partner požádal o mediaci.\n\n{0}\n\nHaveno verze: {1} support.mediatorsDisputeSummary=Systémová zpráva: Shrnutí sporu mediátora:\n{0} +support.mediatorReceivedLogs=Systémová zpráva: Mediátor obdržel logy: {0} support.mediatorsAddress=Adresa uzlu mediátora: {0} -support.warning.disputesWithInvalidDonationAddress=Odložená výplatní transakce použila neplatnou adresu příjemce. Neshoduje se s žádnou z hodnot parametrů DAO pro platné dárcovské adresy.\n\nMůže to být pokus o podvod. Informujte prosím vývojáře o tomto incidentu a neuzavírejte tento případ, dokud nebude situace vyřešena!\n\nAdresa použitá ve sporu: {0}\n\nVšechny parametry pro darovací adresy DAO: {1}\n\nObchodní ID: {2} {3} +support.warning.disputesWithInvalidDonationAddress=Odložená výplatní transakce použila neplatnou adresu příjemce. \ + Neshoduje se s žádnou z hodnot parametrů DAO pro platné dárcovské adresy.\n\nMůže to být pokus o podvod. \ + Informujte prosím vývojáře o tomto incidentu a neuzavírejte tento případ, dokud nebude situace vyřešena!\n\n\ + Adresa použitá ve sporu: {0}\n\n\ + Všechny parametry pro darovací adresy DAO: {1}\n\n\ + Obchodní ID: {2}\ + {3} support.warning.disputesWithInvalidDonationAddress.mediator=\n\nStále chcete spor uzavřít? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nVýplatu nesmíte provést. support.warning.traderCloseOwnDisputeWarning=Obchodníci mohou sami zrušit úkol pro podporu pouze pokud došlo k výplatě prostředků. @@ -984,7 +1277,7 @@ settings.tab.network=Informace o síti settings.tab.about=O Haveno setting.preferences.general=Základní nastavení -setting.preferences.explorer=Monero Explorer +setting.preferences.explorer=Průzkumník Monero setting.preferences.deviation=Max. odchylka od tržní ceny setting.preferences.avoidStandbyMode=Vyhněte se pohotovostnímu režimu setting.preferences.useSoundForNotifications=Přehrávat zvuky pro upozornění @@ -992,12 +1285,10 @@ setting.preferences.autoConfirmXMR=Automatické potvrzení XMR setting.preferences.autoConfirmEnabled=Povoleno setting.preferences.autoConfirmRequiredConfirmations=Požadovaná potvrzení setting.preferences.autoConfirmMaxTradeSize=Max. částka obchodu (XMR) -setting.preferences.autoConfirmServiceAddresses=Monero Explorer URL (používá Tor, kromě localhost, LAN IP adres a názvů hostitele *.local) +setting.preferences.autoConfirmServiceAddresses=Adresa průzkumníka Monero (používá Tor, kromě localhost, LAN IP adres a názvů hostitele *.local) setting.preferences.deviationToLarge=Hodnoty vyšší než {0} % nejsou povoleny. setting.preferences.txFee=Poplatek za výběr transakce (satoshi/vbyte) setting.preferences.useCustomValue=Použijte vlastní hodnotu -setting.preferences.txFeeMin=Transakční poplatek musí být alespoň {0} satoshi/vbyte -setting.preferences.txFeeTooLarge=Váš vstup je nad jakoukoli rozumnou hodnotou (>5000 satoshi/vbyte). Transakční poplatek se obvykle pohybuje v rozmezí 50-400 satoshi/vbyte. setting.preferences.ignorePeers=Ignorované peer uzly [onion addresa:port] setting.preferences.ignoreDustThreshold=Min. hodnota výstupu bez drobných setting.preferences.currenciesInList=Měny v seznamu zdrojů tržních cen @@ -1005,8 +1296,8 @@ setting.preferences.prefCurrency=Preferovaná měna setting.preferences.displayTraditional=Zobrazit národní měny setting.preferences.noTraditional=Nejsou vybrány žádné národní měny setting.preferences.cannotRemovePrefCurrency=Vybranou zobrazovanou měnu nelze odebrat. -setting.preferences.displayCryptos=Zobrazit cryptoy -setting.preferences.noCryptos=Nejsou vybrány žádné cryptoy +setting.preferences.displayCryptos=Zobrazit kryptoměny +setting.preferences.noCryptos=Nejsou vybrány žádné kryptoměny setting.preferences.addTraditional=Přidejte národní měnu setting.preferences.addCrypto=Přidejte crypto setting.preferences.displayOptions=Zobrazit možnosti @@ -1017,33 +1308,43 @@ setting.preferences.sortWithNumOffers=Seřadit seznamy trhů s počtem nabídek/ setting.preferences.onlyShowPaymentMethodsFromAccount=Skrýt nepodporované způsoby platby setting.preferences.denyApiTaker=Odmítat příjemce, kteří používají API setting.preferences.notifyOnPreRelease=Získávat oznámení o beta verzích -setting.preferences.resetAllFlags=Zrušit všechny "Nezobrazovat znovu" +setting.preferences.resetAllFlags=Zrušit všechny \"Nezobrazovat znovu\" settings.preferences.languageChange=Chcete-li použít změnu jazyka na všech obrazovkách, musíte restartovat aplikaci. settings.preferences.supportLanguageWarning=V případě sporu mějte na paměti, že arbitráž je řešena v {0}. settings.preferences.editCustomExplorer.headline=Nastavení Průzkumníku -settings.preferences.editCustomExplorer.description=Ze seznamu vlevo vyberte průzkumníka definovaného systémem nebo si jej přizpůsobte podle svých vlastních preferencí. +settings.preferences.editCustomExplorer.description=Ze seznamu vlevo vyberte průzkumníka definovaného systémem a nebo \ + si jej přizpůsobte podle svých vlastních preferencí. settings.preferences.editCustomExplorer.available=Dostupní průzkumníci settings.preferences.editCustomExplorer.chosen=Nastavení zvoleného průzkumníka settings.preferences.editCustomExplorer.name=Jméno settings.preferences.editCustomExplorer.txUrl=Transakční URL settings.preferences.editCustomExplorer.addressUrl=Adresa URL -settings.net.xmrHeader=Moneroová síť +settings.net.xmrHeader=Síť Monero settings.net.p2pHeader=Síť Haveno settings.net.onionAddressLabel=Moje onion adresa -settings.net.xmrNodesLabel=Použijte vlastní Monero node +settings.net.xmrNodesLabel=Použijte vlastní Monero uzel settings.net.moneroPeersLabel=Připojené peer uzly +settings.net.connection=Připojení +settings.net.connected=Připojeno settings.net.useTorForXmrJLabel=Použít Tor pro Monero síť -settings.net.moneroNodesLabel=Monero nody, pro připojení -settings.net.useProvidedNodesRadio=Použijte nabízené Monero Core nody -settings.net.usePublicNodesRadio=Použít veřejnou Moneroovou síť -settings.net.useCustomNodesRadio=Použijte vlastní Monero Core node -settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero nody, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených nodů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné nody? -settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené nody +settings.net.useTorForXmrAfterSyncRadio=Po synchronizaci peněženky +settings.net.useTorForXmrOffRadio=Nikdy +settings.net.useTorForXmrOnRadio=Vždy +settings.net.moneroNodesLabel=Monero uzly, pro připojení +settings.net.useProvidedNodesRadio=Použít nabízené Monero uzly +settings.net.usePublicNodesRadio=Použít veřejnou síť Monero +settings.net.useCustomNodesRadio=Použít vlastní Monero uzel +settings.net.warn.usePublicNodes=Pokud používáte veřejné Monero uzly, jste vystaveni riziku spojenému s používáním nedůvěryhodných vzdálených uzlů.\n\nProsím, přečtěte si více podrobností na [HYPERLINK:https://www.getmonero.org/resources/moneropedia/remote-node.html].\n\nJste si jistí, že chcete použít veřejné uzly? +settings.net.warn.usePublicNodes.useProvided=Ne, použijte nabízené uzly settings.net.warn.usePublicNodes.usePublic=Ano, použít veřejnou síť -settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš moneroový node je důvěryhodný Monero Core node!\n\nPřipojení k nodům, které nedodržují pravidla konsensu Monero Core, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\nUživatelé, kteří se připojují k nodům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! -settings.net.warn.invalidXmrConfig=Připojení k moneroové síti selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby se místo toho použily poskytnuté moneroové uzly. Budete muset restartovat aplikaci. -settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Moneroový uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. +settings.net.warn.useCustomNodes.B2XWarning=Ujistěte se, že váš Monero uzel je důvěryhodný Monero uzel!\n\n\ + Připojení k uzlům, které nedodržují pravidla konsensu Monero, může poškodit vaši peněženku a způsobit problémy v obchodním procesu.\n\n\ + Uživatelé, kteří se připojují k uzlům, které porušují pravidla konsensu, odpovídají za případné škody, které z toho vyplývají. \ + Jakékoli výsledné spory budou rozhodnuty ve prospěch druhého obchodníka. Uživatelům, kteří ignorují \ + tyto varovné a ochranné mechanismy, nebude poskytována technická podpora! +settings.net.warn.invalidXmrConfig=Připojení k síti Monero selhalo, protože je vaše konfigurace neplatná.\n\nVaše konfigurace byla resetována, aby byly místo toho použity poskytnuté uzly Monero. Budete muset restartovat aplikaci. +settings.net.localhostXmrNodeInfo=Základní informace: Haveno při spuštění hledá místní Monero uzel. Pokud je nalezen, Haveno bude komunikovat se sítí Monero výhradně skrze něj. settings.net.p2PPeersLabel=Připojené uzly settings.net.onionAddressColumn=Onion adresa settings.net.creationDateColumn=Založeno @@ -1067,17 +1368,21 @@ settings.net.sentData=Odeslaná data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.receivedData=Přijatá data: {0}, {1} zprávy, {2} zprávy/sekundu settings.net.chainHeight=Monero Peers: {0} settings.net.ips=[IP adresa:port | název hostitele:port | onion adresa:port] (oddělené čárkou). Pokud je použit výchozí port (8333), lze port vynechat. -settings.net.seedNode=Seed node +settings.net.seedNode=Seed uzel settings.net.directPeer=Peer uzel (přímý) settings.net.initialDataExchange={0} [Bootstrapping] settings.net.peer=Peer settings.net.inbound=příchozí settings.net.outbound=odchozí +settings.net.rescanOutputsLabel=Znovu oskenovat výstupy +settings.net.rescanOutputsButton=Znovu oskenovat výstupy peněženky +settings.net.rescanOutputsSuccess=Jste si jistí, že chcete znovu oskenovat výstupy vaší peněženky? +settings.net.rescanOutputsFailed=Nepodařilo se oskenovat výstupy peněženky.\nChyba: {0} setting.about.aboutHaveno=O projektu Haveno setting.about.about=Haveno je software s otevřeným zdrojovým kódem, který usnadňuje směnu moneroů s národními měnami (a jinými kryptoměnami) prostřednictvím decentralizované sítě typu peer-to-peer způsobem, který silně chrání soukromí uživatelů. Zjistěte více o Haveno na naší webové stránce projektu. setting.about.web=Webová stránka Haveno setting.about.code=Zdrojový kód -setting.about.agpl=AGPL Licence +setting.about.agpl=AGPL licence setting.about.support=Podpořte Haveno setting.about.def=Haveno není společnost - je to projekt otevřený komunitě. Pokud se chcete zapojit nebo podpořit Haveno, postupujte podle níže uvedených odkazů. setting.about.contribute=Přispět @@ -1101,7 +1406,7 @@ 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.closePopup=Zavřete vyskakovací nebo dialogové okno -setting.about.shortcuts.closePopup.value=Klávesa „ESCAPE“ +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'' @@ -1136,20 +1441,17 @@ setting.about.shortcuts.sendFilter=Nastavit filtr (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification=Odeslat soukromé oznámení partnerovi (privilegovaná aktivita) setting.about.shortcuts.sendPrivateNotification.value=Otevřete informace o uživateli kliknutím na avatar a stiskněte: {0} -setting.info.headline=Nová funkce automatického potvrzení XMR -setting.info.msg=Při prodeji XMR za XMR můžete pomocí funkce automatického potvrzení ověřit, že do vaší peněženky bylo odesláno správné množství XMR, takže Haveno může automaticky označit obchod jako dokončený, což zrychlí obchodování pro všechny.\n\nAutomatické potvrzení zkontroluje transakci XMR alespoň na 2 uzlech průzkumníka XMR pomocí klíče soukromé transakce poskytnutého odesílatelem XMR. Ve výchozím nastavení používá Haveno uzly průzkumníka spuštěné přispěvateli Haveno, ale pro maximální soukromí a zabezpečení doporučujeme spustit vlastní uzel průzkumníka XMR.\n\nMůžete také nastavit maximální částku XMR na obchod, která se má automaticky potvrdit, a také počet požadovaných potvrzení zde v Nastavení.\n\nZobrazit další podrobnosti (včetně toho, jak nastavit vlastní uzel průzkumníka) na Haveno wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades] #################################################################### # Account #################################################################### +account.tab.arbitratorRegistration=Registrace rozhodce account.tab.mediatorRegistration=Registrace mediátora account.tab.refundAgentRegistration=Registrace rozhodce pro vrácení peněz account.tab.signing=Podepisování -account.info.headline=Vítejte ve vašem účtu Haveno -account.info.msg=Zde můžete přidat obchodní účty pro národní měny & cryptoy a vytvořit zálohu dat vaší peněženky a účtu.\n\nPři prvním spuštění Haveno byla vytvořena nová moneroová peněženka.\n\nDůrazně doporučujeme zapsat si seed slova moneroových peněženek (viz záložka nahoře) a před financováním zvážit přidání hesla. Vklady a výběry moneroů jsou spravovány v sekci \ "Finance \".\n\nOchrana osobních údajů a zabezpečení: protože Haveno je decentralizovaná směnárna, všechna data jsou uložena ve vašem počítači. Neexistují žádné servery, takže nemáme přístup k vašim osobním informacím, vašim finančním prostředkům ani vaší IP adrese. Údaje, jako jsou čísla bankovních účtů, adresy cryptoů a monerou atd., jsou sdíleny pouze s obchodním partnerem za účelem uskutečnění obchodů, které zahájíte (v případě sporu uvidí Prostředník nebo Rozhodce stejná data jako váš obchodní partner). account.menu.paymentAccount=Účty v národní měně -account.menu.altCoinsAccountView=Cryptoové účty +account.menu.altCoinsAccountView=Kryptoměnové účty account.menu.password=Heslo peněženky account.menu.seedWords=Seed peněženky account.menu.walletInfo=Info o peněžence @@ -1157,11 +1459,16 @@ account.menu.backup=Záloha account.menu.notifications=Oznámení account.menu.walletInfo.balance.headLine=Zůstatky v peněžence -account.menu.walletInfo.balance.info=Zde jsou zobrazeny celkové zůstatky v interní peněžence včetně nepotvrzených transakcí.\nInterní zůstatek XMR uvedený níže by měl odpovídat součtu hodnot 'Dostupný zůstatek' a 'Rezervováno v nabídkách' v pravém horním rohu aplikace. +account.menu.walletInfo.balance.info=Zde jsou zobrazeny celkové zůstatky v interní peněžence včetně nepotvrzených transakcí.\n\ + Interní zůstatek XMR uvedený níže by měl odpovídat součtu hodnot 'Dostupný zůstatek' a 'Rezervováno v nabídkách' v pravém horním rohu aplikace. account.menu.walletInfo.xpub.headLine=Veřejné klíče (xpub) account.menu.walletInfo.walletSelector={0} {1} peněženka account.menu.walletInfo.path.headLine=HD identifikátory klíčů -account.menu.walletInfo.path.info=Pokud importujete vaše seed slova do jiné peněženky (např. Electrum), budete muset nastavit také identifikátor klíčů (BIP32 path). Toto provádějte pouze ve výjimečných případech, např. pokud úplně ztratíte kontrolu nad Haveno peněženkou.\nMějte na paměti, že provádění transakcí pomocí jiných softwarových peněženek může snadno poškodit interní datové struktury systému Haveno, a znemožnit tak provádění obchodů.\n\nNIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Haveno, protože byste tím velmi pravděpodobně vytvořili neplatnou BSQ transakci, a ztratili tak své BSQ. +account.menu.walletInfo.path.info=Pokud importujete vaše seed slova do jiné peněženky (např. Electrum), budete muset nastavit také \ + cestu. Toto provádějte pouze ve výjimečných případech, např. pokud úplně ztratíte kontrolu nad Haveno peněženkou a složkou dat.\n\ + Mějte na paměti, že provádění transakcí pomocí jiných softwarových peněženek může snadno poškodit interní datové struktury \ + systému Haveno, a znemožnit tak provádění obchodů.\n\n\ +NIKDY neposílejte BSQ pomocí jiných softwarových peněženek než Haveno, protože byste tím velmi pravděpodobně vytvořili neplatnou BSQ transakci, a ztratili tak své BSQ. account.menu.walletInfo.openDetails=Zobrazit detailní data peněženky a soukromé klíče @@ -1178,43 +1485,220 @@ account.arbitratorRegistration.removedFailed=Registraci se nepodařilo odebrat. account.arbitratorRegistration.registerSuccess=Úspěšně jste se zaregistrovali do sítě Haveno. account.arbitratorRegistration.registerFailed=Registraci se nepodařilo dokončit. {0} -account.crypto.yourCryptoAccounts=Vaše cryptoové účty -account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo (b) které nepoužívají kompatibilní software peněženky, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce není specialista {2} a v takových případech nemůže pomoci. +account.crypto.yourCryptoAccounts=Vaše kryptoměnové účty +account.crypto.popup.wallet.msg=Ujistěte se, že dodržujete požadavky na používání peněženek {0}, jak je \ +popsáno na webové stránce {1}.\nPoužití peněženek z centralizovaných směnáren, kde (a) nevlastníte své soukromé klíče nebo \ +(b) které nepoužívají kompatibilní software peněženk, je riskantní: může to vést ke ztrátě obchodovaných prostředků!\nMediátor nebo rozhodce \ +není specialista {2} a v takových případech nemůže pomoci. account.crypto.popup.wallet.confirm=Rozumím a potvrzuji, že vím, jakou peněženku musím použít. # suppress inspection "UnusedProperty" -account.crypto.popup.upx.msg=Obchodování s UPX na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání UPX musíte použít buď oficiální peněženku GUI uPlexa nebo CLI peněženku uPlexa s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\nuplexa-wallet-cli (použijte příkaz get_tx_key)\nuplexa-wallet-gui (přejděte na záložku historie a pro potvrzení platby klikněte na tlačítko (P))\n\nV normálním block exploreru není přenos ověřitelný.\n\nV případě sporu musíte rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresa příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. Odesílatel UPX odpovídá za zajištění ověření přenosu UPX rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, vyhledejte další informace na discord kanálu uPlexa (https://discord.gg/vhdNSrV) nebo uPlexa Telegram Chatu (https://t.me/uplexaOfficial). +account.crypto.popup.upx.msg=Obchodování s UPX na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ +K odeslání UPX musíte použít buď oficiální peněženku GUI uPlexa nebo CLI peněženku uPlexa \ +s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ +Ujistěte se, že máte přístup \ +ke klíči tx, který může být vyžadován v případě sporu.\n\ +uplexa-wallet-cli (použijte příkaz get_tx_key)\n\ +uplexa-wallet-gui (přejděte na záložku historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ +V normálním block exploreru není přenos ověřitelný.\n\n\ +V případě sporu musíte rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresa příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. \ +Odesílatel UPX odpovídá za zajištění ověření přenosu UPX rozhodci \ +v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, vyhledejte další informace na discord kanálu uPlexa (https://discord.gg/vhdNSrV) \ +nebo uPlexa Telegram Chatu (https://t.me/uplexaOfficial). # suppress inspection "UnusedProperty" -account.crypto.popup.arq.msg=Obchodování ARQ na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání ARQ musíte použít buď oficiální peněženku ArQmA GUI nebo peněženku ArQmA CLI s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\narqma-wallet-cli (použijte příkaz get_tx_key)\narqma-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\nV normálním blok exploreru není přenos ověřitelný.\n\nV případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresu příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke prohrání sporu. Odesílatel ARQ odpovídá za zajištění ověření převodu ARQ mediátorovi nebo rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, navštivte discord kanál ArQmA (https://discord.gg/s9BQpJT) nebo fórum ArQmA (https://labs.arqma.com). +account.crypto.popup.arq.msg=Obchodování ARQ na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání ARQ musíte použít buď oficiální peněženku ArQmA GUI nebo peněženku ArQmA CLI \ +s povoleným příznakem store-tx-info (výchozí hodnota v nových verzích). \ +Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\n\ +arqma-wallet-cli (použijte příkaz get_tx_key)\n\ +arqma-wallet-gui (přejděte na kartu historie a pro potvrzení platby klikněte na tlačítko (P))\n\n\ +V normálním prohlížeči bloků není přenos ověřitelný.\n\n\ +V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde k prohrání sporu. \ +Odesílatel ARQ odpovídá za zajištění ověření převodu ARQ mediátorovi \ +nebo rozhodci v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, navštivte discord kanál ArQmA (https://discord.gg/s9BQpJT) \ +nebo fórum ArQmA (https://labs.arqma.com). # suppress inspection "UnusedProperty" -account.crypto.popup.xmr.msg=Obchodování s XMR na Haveno vyžaduje, abyste pochopili následující požadavek.\n\nPokud prodáváte XMR, musíte být schopni v případě sporu poskytnout mediátorovi nebo rozhodci následující informace:\n- transakční klíč (Tx klíč, Tx tajný klíč nebo Tx soukromý klíč)\n- ID transakce (Tx ID nebo Tx Hash)\n- cílová adresa (adresa příjemce)\n\nNa wiki najdete podrobnosti, kde najdete tyto informace v populárních peněženkách Monero:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\nNeposkytnutí požadovaných údajů o transakci bude mít za následek ztrátu sporů.\n\nVšimněte si také, že Haveno nyní nabízí automatické potvrzení transakcí XMR, aby byly obchody rychlejší, ale musíte to povolit v Nastavení.\n\nDalší informace o funkci automatického potvrzení najdete na wiki:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. +account.crypto.popup.xmr.msg=Obchodování s XMR na Haveno vyžaduje, abyste pochopili následující požadavek.\n\n\ +Pokud prodáváte XMR, musíte být schopni v případě sporu poskytnout mediátorovi nebo rozhodci následující informace:\n\ +- transakční klíč (Tx klíč, Tx tajný klíč nebo Tx soukromý klíč)\n\ +- ID transakce (Tx ID nebo Tx Hash)\n\ +- cílová adresa (adresa příjemce)\n\n\ +Na wiki najdete podrobnosti, kde najdete tyto informace v populárních peněženkách Monero:\n[HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Proving_payments].\n\ +Neposkytnutí požadovaných údajů o transakci bude mít za následek ztrátu sporů.\n\n\ +Všimněte si také, že Haveno nyní nabízí automatické potvrzení transakcí XMR, aby byly obchody rychlejší, \ +ale musíte to povolit v Nastavení.\n\n\ +Další informace o funkci automatického potvrzení najdete na wiki: [HYPERLINK:https://haveno.exchange/wiki/Trading_Monero#Auto-confirming_trades]. # suppress inspection "UnusedProperty" -account.crypto.popup.msr.msg=Obchodování MSR na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání MSR musíte použít buď oficiální peněženku Masari GUI, peněženku Masari CLI s povoleným příznakem store-tx-info (ve výchozím nastavení povoleno) nebo webovou peněženku Masari (https://wallet.getmasari.org). Ujistěte se, že máte přístup ke klíči tx, který může být vyžadován v případě sporu.\nmasari-wallet-cli (použijte příkaz get_tx_key)\nmasari-wallet-gui (přejděte na kartu historie a klikněte na tlačítko (P) pro potvrzení platby)\n\nWebová peněženka Masari (jděte do Účet -> Historie transakcí a zobrazte podrobností o odeslané transakci)\n\nOvěření lze provést v peněžence.\nmasari-wallet-cli: pomocí příkazu (check_tx_key).\nmasari-wallet-gui: na stránce Pokročilé > Dokázat/Ověřit.\nOvěření lze provést v block exploreru\nOtevřete Block explorer (https://explorer.getmasari.org), použijte vyhledávací lištu k nalezení hash transakce.\nJakmile je transakce nalezena, přejděte dolů do oblasti „Prokázat odesílání“ a podle potřeby vyplňte podrobnosti.\nV případě sporu musíte zprostředkovateli nebo rozhodci poskytnout následující údaje:\n- Soukromý klíč tx\n- Hash transakce\n- Veřejnou adresu příjemce\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. Odesílatel MSR odpovídá za zajištění ověření přenosu MSR mediátorovi nebo rozhodci v případě sporu.\n\nNení požadováno žádné platební ID, pouze normální veřejná adresa.\nPokud si nejste jisti tímto procesem, požádejte o pomoc oficiální Masari Discord (https://discord.gg/sMCwMqs). +account.crypto.popup.msr.msg=Obchodování MSR na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +K odeslání MSR musíte použít buď oficiální peněženku Masari GUI, peněženku Masari CLI s povoleným příznakem \ +store-tx-info (ve výchozím nastavení povoleno) nebo webovou peněženku Masari (https://wallet.getmasari.org). Ujistěte se, že máte přístup ke klíči tx, \ +který může být vyžadován v případě sporu.\n\ +masari-wallet-cli (použijte příkaz get_tx_key)\n\ +masari-wallet-gui (přejděte na kartu historie a klikněte na tlačítko (P) pro potvrzení platby)\n\n\ +Webová peněženka Masari (jděte do Účet -> Historie transakcí a zobrazte podrobností o odeslané transakci)\n\n\ +Ověření lze provést v peněžence.\n\ +masari-wallet-cli: pomocí příkazu (check_tx_key).\n\ +masari-wallet-gui: na stránce Pokročilé > Dokázat/Ověřit.\n\ +Ověření lze provést v block exploreru\n\ +Otevřete Block explorer (https://explorer.getmasari.org), použijte vyhledávací lištu k nalezení hash transakce.\n\ +Jakmile je transakce nalezena, přejděte dolů do oblasti 'Prokázat odesílání' a podle potřeby vyplňte podrobnosti.\n\ +V případě sporu musíte zprostředkovateli nebo rozhodci poskytnout následující údaje:\n\ +- Soukromý klíč tx\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ +Odesílatel MSR odpovídá za zajištění ověření přenosu MSR mediátorovi \ +nebo rozhodci v případě sporu.\n\n\ +Není požadováno žádné platební ID, pouze normální veřejná adresa.\n\ +Pokud si nejste jisti tímto procesem, požádejte o pomoc oficiální Masari Discord (https://discord.gg/sMCwMqs). # suppress inspection "UnusedProperty" -account.crypto.popup.blur.msg=Obchodování BLUR na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání BLUR musíte použít Blur Network CLI nebo GUI peněženku.\n\nPoužíváte-li peněženku CLI, po odeslání transakce se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\nPokud používáte peněženku GUI Blur Network, lze soukromý klíč transakce a ID transakce pohodlně nalézt na kartě Historie. Ihned po odeslání vyhledejte příslušnou transakci. Klikněte na "?" symbol v pravém dolním rohu pole obsahující transakci. Tyto informace si musíte uložit.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, 2.) soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří přenos BLUR pomocí prohlížeče BLUR transakcí (https://blur.cash/#tx-viewer).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel BLUR 100% odpovědnosti za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). +account.crypto.popup.blur.msg=Obchodování BLUR na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání BLUR musíte použít Blur Network CLI nebo GUI peněženku.\n\n\ +Používáte-li peněženku CLI, po odeslání transakce se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ +Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ +Pokud používáte peněženku GUI Blur Network, lze soukromý klíč transakce a ID transakce pohodlně nalézt \ +na kartě Historie. Ihned po odeslání vyhledejte příslušnou transakci. Klikněte na "?" symbol \ +v pravém dolním rohu pole obsahující transakci. Tyto informace si musíte uložit.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ +2.) soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří přenos BLUR \ +pomocí prohlížeče BLUR transakcí (https://blur.cash/#tx-viewer).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel BLUR 100% odpovědnosti za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Blur Network Discord (https://discord.gg/dMWaqVW). # suppress inspection "UnusedProperty" -account.crypto.popup.solo.msg=Obchodování Solo na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání Solo musíte použít peněženku CLI Solo Network.\n\nPoužíváte-li peněženku CLI, po odeslání přenosu se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. Ihned po odeslání převodu musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, 2.) Soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří převod Solo pomocí Solo Block Exploreru vyhledáním transakce a poté pomocí funkce „Prokažte odesílání“ (https://explorer.minesolo.com/).\n\nneposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese Solo odesílatel 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). +account.crypto.popup.solo.msg=Obchodování Solo na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +K odeslání Solo musíte použít peněženku CLI Solo Network.\n\n\ +Používáte-li peněženku CLI, po odeslání přenosu se zobrazí hash transakce (tx ID). Tyto informace si musíte uložit. \ +Ihned po odeslání převodu musíte použít příkaz 'get_tx_key' pro načtení soukromého klíče transakce. \ +Pokud tento krok neprovedete, pravděpodobně nebudete moci klíč získat později.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1.) ID transakce, \ +2.) Soukromý klíč transakce a 3.) Adresu příjemce. Mediátor nebo rozhodce poté ověří převod Solo \ +pomocí Solo Block Exploreru vyhledáním transakce a poté pomocí funkce 'Prokažte odesílání' (https://explorer.minesolo.com/).\n\n\ +neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese Solo odesílatel 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce Solo Network Discord (https://discord.minesolo.com/). # suppress inspection "UnusedProperty" -account.crypto.popup.cash2.msg=Obchodování CASH2 na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání CASH2 musíte použít peněženku Cash2 Wallet verze 3 nebo vyšší.\n\nPo odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'getTxKey' v simplewallet a získat tajný klíč transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, 2) Tajný klíč transakce a 3) Adresu Cash2 příjemce. Mediátor nebo rozhodce poté ověří převod CASH2 pomocí průzkumníku Cash2 Block Explorer (https://blocks.cash2.org).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel CASH2 100% odpovědnost za ověření transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). +account.crypto.popup.cash2.msg=Obchodování CASH2 na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání CASH2 musíte použít peněženku Cash2 Wallet verze 3 nebo vyšší.\n\n\ +Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'getTxKey' v simplewallet \ +a získat tajný klíč transakce.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ +2) Tajný klíč transakce a 3) Adresu Cash2 příjemce. Mediátor nebo rozhodce poté ověří převod CASH2 \ +pomocí průzkumníku Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel CASH2 100% odpovědnost za ověření transakcí mediátorovi nebo rozhodci. \n\n +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na Cash2 Discord (https://discord.gg/FGfXAYN). # suppress inspection "UnusedProperty" -account.crypto.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání QWC musíte použít oficiální QWC peněženku verze 5.1.3 nebo vyšší.\n\nPo odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. Ihned po odeslání transakce musíte použít příkaz 'get_Tx_Key' v simplewallet a získat tajný klíč transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, 2) Tajný klíč transakce a 3) Adresu QWC příjemce. Mediátor nebo rozhodce poté ověří přenos QWC pomocí Průzkumníka bloků QWC (https://explorer.qwertycoin.org).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel QWC 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). +account.crypto.popup.qwertycoin.msg=Obchodování s Qwertycoinem na Haveno vyžaduje, abyste pochopili \ +a splnili následující požadavky:\n\n\ +K odeslání QWC musíte použít oficiální QWC peněženku verze 5.1.3 nebo vyšší.\n\n\ +Po odeslání transakce se zobrazí ID transakce. Tyto informace si musíte uložit. \ +Ihned po odeslání transakce musíte použít příkaz 'get_Tx_Key' v simplewallet \ +a získat tajný klíč transakce.\n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit následující: 1) ID transakce, \ +2) Tajný klíč transakce a 3) Adresu QWC příjemce. Mediátor nebo rozhodce poté ověří přenos QWC \ +pomocí Průzkumníka bloků QWC (https://explorer.qwertycoin.org).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. \ +Ve všech sporných případech nese odesílatel QWC 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na stránce QWC Discord (https://discord.gg/rUkfnpC). # suppress inspection "UnusedProperty" -account.crypto.popup.drgl.msg=Obchodování Dragonglass na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nVzhledem k tomu, že Dragonglass poskytuje soukromí, není transakce na veřejném blockchainu ověřitelná. V případě potřeby můžete svou platbu prokázat pomocí vašeho soukromého klíče TXN.\nSoukromý klíč TXN je jednorázový klíč automaticky generovaný pro každou transakci, ke které lze přistupovat pouze z vaší DRGL peněženky.\nBuď pomocí GUI peněženky DRGL (uvnitř dialogu s podrobnostmi o transakci) nebo pomocí simplewallet CLI Dragonglass (pomocí příkazu "get_tx_key").\n\nVerze DRGL „Oathkeeper“ a vyšší jsou požadovány pro obě možnosti.\n\nV případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n- TXN-soukromý klíč\n- Hash transakce\n- Veřejnou adresu příjemce\n\nOvěření platby lze provést pomocí výše uvedených údajů jako vstupů na adrese (http://drgl.info/#check_txn).\n\nPokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. Odesílatel Dragonglass odpovídá za ověření přenosu DRGL mediátorovi nebo rozhodci v případě sporu. Použití PaymentID není nutné.\n\nPokud si nejste jisti některou částí tohoto procesu, navštivte nápovědu pro Dragonglass na Discord (http://discord.drgl.info). +account.crypto.popup.drgl.msg=Obchodování Dragonglass na Haveno vyžaduje, abyste pochopili a splnili \ +následující požadavky:\n\n\ +Vzhledem k tomu, že Dragonglass poskytuje soukromí, není transakce na veřejném blockchainu ověřitelná. V případě potřeby \ +můžete svou platbu prokázat pomocí vašeho soukromého klíče TXN.\n\ +Soukromý klíč TXN je jednorázový klíč automaticky generovaný pro každou transakci, \ +ke které lze přistupovat pouze z vaší DRGL peněženky.\n\ +Buď pomocí GUI peněženky DRGL (uvnitř dialogu s podrobnostmi o transakci) nebo pomocí simplewallet CLI Dragonglass (pomocí příkazu "get_tx_key").\n\n\ +Verze DRGL 'Oathkeeper' a vyšší jsou požadovány pro obě možnosti.\n\n\ +V případě sporu musíte mediátorovi nebo rozhodci poskytnout následující údaje:\n\ +- TXN-soukromý klíč\n\ +- Hash transakce\n\ +- Veřejnou adresu příjemce\n\n +Ověření platby lze provést pomocí výše uvedených údajů jako vstupů na adrese (http://drgl.info/#check_txn).\n\n\ +Pokud neposkytnete výše uvedená data nebo použijete nekompatibilní peněženku, dojde ke ztrátě sporu. \ +Odesílatel Dragonglass odpovídá za ověření přenosu DRGL mediátorovi nebo rozhodci v případě sporu. \ +Použití PaymentID není nutné.\n\n +Pokud si nejste jisti některou částí tohoto procesu, navštivte Dragonglass na Discordu (http://discord.drgl.info) pro pomoc. # suppress inspection "UnusedProperty" -account.crypto.popup.ZEC.msg=Při použití Zcash můžete použít pouze transparentní adresy (začínající t), nikoli z-adresy (soukromé), protože mediátor nebo rozhodce by nemohl ověřit transakci pomocí z-adres. +account.crypto.popup.ZEC.msg=Při použití Zcash můžete použít pouze transparentní adresy (začínající na t), nikoli \ +z-adresy (soukromé), protože mediátor nebo rozhodce by nemohl ověřit transakci pomocí z-adres. # suppress inspection "UnusedProperty" -account.crypto.popup.XZC.msg=Při použití Zcoinu můžete použít pouze transparentní (sledovatelné) adresy, nikoli nevysledovatelné adresy, protože mediátor nebo rozhodce by nemohl ověřit transakci s nevysledovatelnými adresami v blok exploreru. +account.crypto.popup.XZC.msg=Při použití Zcoinu můžete použít pouze transparentní (sledovatelné) adresy, \ +nikoli nevysledovatelné adresy, protože mediátor nebo rozhodce by nemohl ověřit transakci s nevysledovatelnými adresami v blok exploreru. # suppress inspection "UnusedProperty" -account.crypto.popup.grin.msg=GRIN vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem. Nezapomeňte postupovat podle pokynů z webové stránky projektu GRIN, abyste spolehlivě odeslali a přijali GRIN (příjemce musí být online nebo alespoň online v určitém časovém rozmezí).\n\nHaveno podporuje pouze formát URL peněženky Grinbox (Wallet713).\n\nOdesílatel GRIN je povinen prokázat, že GRIN úspěšně odeslal. Pokud peněženka nemůže tento důkaz poskytnout, bude potenciální spor vyřešen ve prospěch příjemce GRIN. Ujistěte se, že používáte nejnovější software Grinbox, který podporuje důkaz transakcí a že chápete proces přenosu a přijímání GRIN a také způsob, jak vytvořit důkaz.\n\nViz https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only pro více informací o nástroji Grinbox proof. +account.crypto.popup.grin.msg=GRIN vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem. \ + Nezapomeňte postupovat podle pokynů z webové stránky projektu GRIN, abyste spolehlivě odeslali a přijali GRIN \ + (příjemce musí být online nebo alespoň online v určitém časovém rozmezí).\n\n\ + Haveno podporuje pouze formát URL peněženky Grinbox (Wallet713).\n\n\ + Odesílatel GRIN je povinen prokázat, že GRIN úspěšně odeslal. Pokud peněženka nemůže tento důkaz poskytnout, \ + bude potenciální spor vyřešen ve prospěch příjemce GRIN. Ujistěte se, že používáte \ + nejnovější software Grinbox, který podporuje důkaz transakcí a že chápete proces přenosu \ + a přijímání GRIN a také způsob, jak vytvořit důkaz.\n\n\ + Viz https://github.com/vault713/wallet713/blob/master/docs/usage.md#transaction-proofs-grinbox-only \ +pro více informací o nástroji Grinbox proof. # suppress inspection "UnusedProperty" -account.crypto.popup.beam.msg=BEAM vyžaduje k vytvoření transakce interaktivní proces mezi odesílatelem a příjemcem.\n\nNezapomeňte postupovat podle pokynů na webové stránce projektu BEAM, abyste spolehlivě odeslali a přijali BEAM (příjemce musí být online nebo alespoň online během určitého časového období).\n\nOdesílatel BEAM je povinen prokázat, že úspěšně odeslali BEAM. Nezapomeňte použít software peněženku, která může takový důkaz předložit. Pokud peněženka nemůže poskytnout důkaz, bude potenciální spor vyřešen ve prospěch příjemce BEAM. +account.crypto.popup.beam.msg=BEAM vyžaduje k vytvoření transakce interaktivní proces \ + mezi odesílatelem \ + a příjemcem.\n\n\ + Nezapomeňte postupovat podle pokynů na webové stránce projektu BEAM, abyste spolehlivě odeslali a přijali BEAM \ + (příjemce musí být online nebo alespoň online během určitého časového období).\n\n\ + Odesílatel BEAM je povinen prokázat, že úspěšně odeslali BEAM. \ + Nezapomeňte použít software peněženku, která může takový důkaz předložit. Pokud peněženka nemůže poskytnout důkaz, \ + bude potenciální spor vyřešen ve prospěch příjemce BEAM. # suppress inspection "UnusedProperty" -account.crypto.popup.pars.msg=Trading ParsiCoin na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\nK odeslání PARS musíte použít oficiální ParsiCoin peněženku verze 3.0.0 nebo vyšší.\n\nV Peněženka GUI (ParsiPay) si můžete zkontrolovat svůj Hash Transakce a Klíč Transakce v sekci Transakce.\n\nV případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit: 1) Hash Transakce, 2) Transakční Klíč a 3) Adresu PARS příjemce. Mediátor nebo rozhodce poté ověří přenos PARS pomocí Block exploreru ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\nNeposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese odesílatel ParsiCoin 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\nPokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). +account.crypto.popup.pars.msg=Trading ParsiCoin na Haveno vyžaduje, abyste pochopili a splnili následující požadavky:\n\n\ +K odeslání PARS musíte použít oficiální ParsiCoin peněženku verze 3.0.0 nebo vyšší.\n\n\ +V Peněženka GUI (ParsiPay) si můžete zkontrolovat svůj Hash Transakce a Klíč Transakce v sekci Transakce. \ +Je zapotřebí kliknout na transakci a potom na zobrazení detailů. \n\n\ +V případě, že je nutné rozhodčí řízení, musíte mediátorovi nebo rozhodci předložit: 1) Hash Transakce, \ +2) Transakční Klíč a 3) Adresu PARS příjemce. Mediátor nebo rozhodce poté ověří přenos PARS \ +pomocí Block exploreru ParsiCoin (http://explorer.parsicoin.net/#check_payment).\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo rozhodci povede k prohrání sporu. Ve všech sporných případech nese \ +odesílatel ParsiCoin 100% odpovědnost za ověřování transakcí mediátorovi nebo rozhodci.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte na Haveno. Nejprve vyhledejte pomoc na ParsiCoin Discord (https://discord.gg/c7qmFNh). # suppress inspection "UnusedProperty" -account.crypto.popup.blk-burnt.msg=Chcete-li obchodovat s burnt blackcoiny, musíte znát následující:\n\nBurnt blackcoiny jsou nevyčerpatelné. Aby je bylo možné obchodovat na Haveno, musí mít výstupní skripty podobu: OP_RETURN OP_PUSHDATA, následované přidruženými datovými bajty, které po hexadecimálním zakódování tvoří adresy. Například Burnt blackcoiny s adresou 666f6f („foo“ v UTF-8) budou mít následující skript:\n\nOP_RETURN OP_PUSHDATA 666f6f\n\nPro vytvoření Burnt blackcoinů lze použít příkaz „burn“ RPC, který je k dispozici v některých peněženkách.\n\nPro možné případy použití se můžete podívat na https://ibo.laboratorium.ee.\n\nVzhledem k tomu, že Burnt blackcoiny jsou nevyčerpatelné, nelze je znovu prodat. „Prodej“ Burnt blackcoinů znamená vypalování běžných blackcoinů (s přidruženými údaji rovnými cílové adrese).\n\nV případě sporu musí prodejce BLK poskytnout hash transakce. +account.crypto.popup.blk-burnt.msg=Chcete-li obchodovat s burnt blackcoiny, musíte znát následující:\n\n\ +Burnt blackcoiny jsou nevyčerpatelné. Aby je bylo možné obchodovat na Haveno, musí mít výstupní skripty podobu: \ +OP_RETURN OP_PUSHDATA, následované přidruženými datovými bajty, které po hexadecimálním zakódování tvoří adresy. \ +Například Burnt blackcoiny s adresou 666f6f (“foo” v UTF-8) budou mít následující skript:\n\n\ +OP_RETURN OP_PUSHDATA 666f6f\n\n\ +Pro vytvoření Burnt blackcoinů lze použít příkaz ”burn” RPC, který je k dispozici v některých peněženkách.\n\n\ +Pro možné případy použití se můžete podívat na https://ibo.laboratorium.ee.\n\n\ +Vzhledem k tomu, že Burnt blackcoiny jsou nevyčerpatelné, nelze je znovu prodat. ”Prodej” \ +Burnt blackcoinů znamená vypalování běžných blackcoinů (s přidruženými údaji rovnými cílové adrese).\n\n\ +V případě sporu musí prodejce BLK poskytnout hash transakce. # suppress inspection "UnusedProperty" -account.crypto.popup.liquidbitcoin.msg=Obchodování s L-XMR na Haveno vyžaduje, abyste pochopili následující skutečnosti:\n\nKdyž přijímáte L-XMR za obchod na Haveno, nemůžete použít mobilní peněženku Green od Blockstreamu ani jinou custodial peněženku nebo peněženku na burze. L-XMR musíte přijmout pouze do peněženky Liquid Elements Core nebo do jiné L-XMR peněženky, která vám umožní získat slepý klíč pro vaši slepou adresu L-XMR.\n\nV případě, že je nutné zprostředkování, nebo pokud dojde k obchodnímu sporu, musíte zprostředkujícímu mediátorovi Haveno nebo agentovi, který vrací peníze zaslat slepý klíč pro vaši L-XMR adresu, aby mohli ověřit podrobnosti vaší důvěrné transakce na svém vlastním Elements Core full-nodu.\n\nNeposkytnutí požadovaných informací zprostředkovateli nebo agentovi pro vrácení peněz povede ke prohrání sporu. Ve všech sporných případech nese příjemce L-XMR 100% břemeno odpovědnosti za poskytnutí kryptografického důkazu zprostředkovateli nebo agentovi pro vrácení peněz.\n\nPokud těmto požadavkům nerozumíte, neobchodujte s L-XMR na Haveno. +account.crypto.popup.liquidmonero.msg=Obchodování s L-XMR na Haveno vyžaduje, abyste rozuměli následujícím skutečnostem.:\n\n\ +Při přijímání L-XMR za obchod na platformě Haveno nemůžete použít mobilní aplikaci Blockstream Green Wallet nebo \ +peněženku custodial/směnárny. L-XMR musíte přijímat pouze do peněženky Liquid Elements Core nebo do jiné \ +L-XMR peněženky, která vám umožní získat zaslepovací(blinding) klíč pro vaši zaslepenou adresu L-XMR.\n\n\ +V případě nutnosti mediace nebo v případě vzniku obchodního sporu je nutné zveřejnit zaslepovací klíč pro \ +vaši přijímající adresu L-XMR mediátorovi Haveno nebo agentovi vrácení financí, aby mohl ověřit detaily \ +vaší důvěrné transakce na vlastním Elements Core full node.\n\n\ +Neposkytnutí požadovaných informací zprostředkovateli nebo agentovi vrácení financí, bude mít za následek ztrátu sporu. \ +Ve všech případech sporu nese příjemce L-XMR 100% břemeno odpovědnosti \ +poskytnutí kryptografického důkazu mediátorovi nebo agentovi pro vrácení financí.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte s L-XMR na Havenu. account.traditional.yourTraditionalAccounts=Vaše účty v národní měně @@ -1232,17 +1716,27 @@ account.password.removePw.button=Odstraňte heslo account.password.removePw.headline=Odstraňte ochranu peněženky pomocí hesla account.password.setPw.button=Nastavit heslo account.password.setPw.headline=Nastavte ochranu peněženky pomocí hesla -account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení vašich slov z klíčového základu. - +account.password.info=S ochranou pomocí hesla budete muset zadat heslo při spuštění aplikace, při výběru monera z vaší peněženky a při zobrazení slov seedu peněženky. account.seed.backup.title=Zálohujte svá klíčová slova peněženky. -account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky "Účet/Záloha", abyste mohli obnovit stav a data aplikace. -account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejsou náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky "Účet/Záloha", abyste mohli obnovit stav a data aplikace. -account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\nChcete zobrazit seed slova? +account.seed.info=Prosím, zapište si jak klíčová slova peněženky, tak datum. Kdykoliv můžete obnovit svou peněženku pomocí klíčových slov a data.\n\nKlíčová slova byste měli zapsat na kus papíru. Neukládejte je na počítač.\n\nVezměte prosím na vědomí, že klíčová slova NEJSOU náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. +account.seed.backup.warning=Prosím, poznamenejte si, že klíčová slova nejsou náhradou za zálohu.\nMusíte vytvořit zálohu celého adresáře aplikace z obrazovky \"Účet/Záloha\", abyste mohli obnovit stav a data aplikace. +account.seed.warn.noPw.msg=Nenastavili jste si heslo k peněžence, které by chránilo zobrazení seed slov.\n\n\ +Chcete zobrazit seed slova? account.seed.warn.noPw.yes=Ano, a už se mě znovu nezeptat account.seed.enterPw=Chcete-li zobrazit seed slova, zadejte heslo -account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\nNení to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace použijte zálohu z adresáře dat aplikace.\n\nPo obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat s moneroovou sítí. To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. Vyhněte se přerušování tohoto procesu, jinak budete možná muset znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. +account.seed.restore.info=Před použitím obnovení ze seed slov si vytvořte zálohu. Uvědomte si, že obnova peněženky je \ + pouze pro naléhavé případy a může způsobit problémy s interní databází peněženky.\n\ + Není to způsob, jak použít zálohu! K obnovení předchozího stavu aplikace \ + použijte zálohu z adresáře dat aplikace.\n\n\ + Po obnovení se aplikace automaticky vypne. Po restartování aplikace se bude znovu synchronizovat se sítí Monero. \ + To může chvíli trvat a může spotřebovat hodně CPU, zejména pokud byla peněženka starší a měla mnoho transakcí. \ + Vyhněte se přerušování tohoto procesu, jinak budete možná muset \ + znovu odstranit soubor řetězu SPV nebo opakovat proces obnovy. account.seed.restore.ok=Dobře, proveďte obnovu a vypněte Haveno - +account.keys.clipboard.warning=Upozorňujeme, že soukromé klíče peněženky jsou velmi citlivé finanční údaje.\n\n\ + ● Nikomu, kdo vás o ně požádá, byste neměli sdělovat své klíče, pokud si nejste naprosto jisti, že mu můžete důvěřovat při nakládání s vašimi penězi! \n\n\ + ● Data soukromých klíčů byste NEMĚLI kopírovat do schránky, pokud si nejste naprosto jisti, že používáte zabezpečené počítačové prostředí bez rizik malwaru. \n\n\ + Mnoho lidí tímto způsobem přišlo o své Monero. Pokud máte JAKÉKOLI pochybnosti, okamžitě zavřete tento dialog a vyhledejte pomoc někoho znalého. #################################################################### # Mobile notifications @@ -1270,7 +1764,8 @@ account.notifications.priceAlert.low.label=Upozorněte, pokud bude cena XMR pod account.notifications.priceAlert.setButton=Nastavit upozornění na cenu account.notifications.priceAlert.removeButton=Odstraňte upozornění na cenu account.notifications.trade.message.title=Obchodní stav se změnil -account.notifications.trade.message.msg.conf=Vkladová transakce pro obchod s ID {0} je potvrzena. Otevřete prosím svou aplikaci Haveno a začněte s platbou. +account.notifications.trade.message.msg.conf=Vkladová transakce pro obchod s ID {0} je potvrzena. \ + Otevřete prosím svou aplikaci Haveno a začněte s platbou. account.notifications.trade.message.msg.started=Kupující XMR zahájil platbu za obchod s ID {0}. account.notifications.trade.message.msg.completed=Obchod s ID {0} je dokončen. account.notifications.offer.message.title=Vaše nabídka byla přijata @@ -1284,7 +1779,10 @@ account.notifications.marketAlert.offerType.label=Typ nabídky, o kterou mám z account.notifications.marketAlert.offerType.buy=Nákupní nabídky (Chci prodat XMR) account.notifications.marketAlert.offerType.sell=Prodejní nabídky (Chci si koupit XMR) account.notifications.marketAlert.trigger=Nabídková cenová vzdálenost (%) -account.notifications.marketAlert.trigger.info=Když je nastavena cenová vzdálenost, obdržíte upozornění pouze v případě, že je zveřejněna nabídka, která splňuje (nebo překračuje) vaše požadavky. Příklad: chcete prodat XMR, ale budete prodávat pouze s 2% přirážkou k aktuální tržní ceně. Nastavení tohoto pole na 2% zajistí, že budete dostávat upozornění pouze na nabídky s cenami, které jsou o 2% (nebo více) nad aktuální tržní cenou. +account.notifications.marketAlert.trigger.info=Když je nastavena cenová vzdálenost, obdržíte upozornění pouze v případě, \ +že je zveřejněna nabídka, která splňuje (nebo překračuje) vaše požadavky. Příklad: chcete prodat XMR, \ +ale budete prodávat pouze s 2% přirážkou k aktuální tržní ceně. Nastavení tohoto pole na 2% zajistí, \ +že budete dostávat upozornění pouze na nabídky s cenami, které jsou o 2% (nebo více) nad aktuální tržní cenou. account.notifications.marketAlert.trigger.prompt=Procentní vzdálenost od tržní ceny (např. 2,50%, -0,50% atd.) account.notifications.marketAlert.addButton=Přidat upozornění na nabídku account.notifications.marketAlert.manageAlertsButton=Spravovat upozornění na nabídku @@ -1295,10 +1793,13 @@ 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 nabídky Haveno byla zveřejněna nová nabídka ''{0} {1}'' s cenou {2} ({3} {4} tržní cena) a způsob platby ''{5}''.\nID nabídky: {6}. +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} -account.notifications.noWebCamFound.warning=Nebyla nalezena žádná webkamera.\n\nPoužijte e-mailu k odeslání tokenu a šifrovacího klíče z vašeho mobilního telefonu do aplikace Haveno. +account.notifications.noWebCamFound.warning=Nebyla nalezena žádná webkamera.\n\n\ + Použijte e-mailu k odeslání tokenu a šifrovacího klíče z vašeho mobilního telefonu do aplikace Haveno. account.notifications.priceAlert.warning.highPriceTooLow=Vyšší cena musí být větší než nižší cena. account.notifications.priceAlert.warning.lowerPriceTooHigh=Nižší cena musí být nižší než vyšší cena. @@ -1311,7 +1812,7 @@ inputControlWindow.balanceLabel=Dostupný zůstatek contractWindow.title=Podrobnosti o sporu contractWindow.dates=Datum nabídky / Datum obchodu -contractWindow.xmrAddresses=Moneroová adresa kupujícího XMR / prodávajícího XMR +contractWindow.xmrAddresses=Monero adresa kupujícího XMR / prodávajícího XMR contractWindow.onions=Síťová adresa kupující XMR / prodávající XMR contractWindow.accountAge=Stáří účtu XMR kupující / XMR prodejce contractWindow.numDisputes=Počet sporů XMR kupující / XMR prodejce @@ -1330,10 +1831,14 @@ displayUpdateDownloadWindow.button.downloadLater=Stáhnout později displayUpdateDownloadWindow.button.ignoreDownload=Ignorovat tuto verzi displayUpdateDownloadWindow.headline=K dispozici je nová aktualizace Haveno! displayUpdateDownloadWindow.download.failed.headline=Stahování selhalo -displayUpdateDownloadWindow.download.failed=Stažení se nezdařilo.\nStáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\nStáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] -displayUpdateDownloadWindow.success=Nová verze byla úspěšně stažena a podpis ověřen.\n\nOtevřete adresář ke stažení, vypněte aplikaci a nainstalujte novou verzi. +displayUpdateDownloadWindow.download.failed=Stažení se nezdařilo.\n\ + Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.installer.failed=Nelze určit správný instalační program. Stáhněte a ručně ověřte na adrese \ + [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.verify.failed=Ověření se nezdařilo.\n\ + Stáhněte a ručně ověřte na adrese [HYPERLINK:https://haveno.exchange/downloads] +displayUpdateDownloadWindow.success=Nová verze byla úspěšně stažena a podpis ověřen.\n\n\ + Otevřete adresář ke stažení, vypněte aplikaci a nainstalujte novou verzi. displayUpdateDownloadWindow.download.openDir=Otevřít adresář ke stažení disputeSummaryWindow.title=Souhrn @@ -1349,7 +1854,7 @@ disputeSummaryWindow.payoutAmount.invert=Poražený ve sporu odesílá transakci disputeSummaryWindow.reason=Důvod sporu disputeSummaryWindow.tradePeriodEnd=Konec obchodního období disputeSummaryWindow.extraInfo=Detailní informace -disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status +disputeSummaryWindow.delayedPayoutStatus=Stav zpožděné transakce # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" @@ -1383,26 +1888,47 @@ disputeSummaryWindow.close.button=Zavřít úkol # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" -disputeSummaryWindow.close.msg=Ticket uzavřen {0}\n{1} adresa uzlu: {2}\n\nSouhrn:\nObchodní ID: {3}\nMěna: {4}\nVýše obchodu: {5}\nVýplatní částka pro kupujícího XMR: {6}\nVýplatní částka pro prodejce XMR: {7}\n\nDůvod sporu: {8}\n\nSouhrnné poznámky:\n{9}\n +disputeSummaryWindow.close.msg=Úkol uzavřen {0}\n\ + {1} adresa uzlu: {2}\n\n\ + Souhrn:\n\ + Obchodní ID: {3}\n\ + Měna: {4}\n\ + Důvod sporu: {5}\n\n\ + Výše obchodu: {6}\n\ + Výplatní částka pro kupujícího XMR: {7}\n\ + Výplatní částka pro prodejce XMR: {8}\n\n\ + Souhrnné poznámky:\n{9}\n # Do no change any line break or order of tokens as the structure is used for signature verification disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} -disputeSummaryWindow.close.nextStepsForMediation=\nDalší kroky:\nOtevřete obchod a přijměte nebo odmítněte návrhy od mediátora -disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nDalší kroky:\nNevyžadují se od vás žádné další kroky. Pokud rozhodce rozhodl ve váš prospěch, v sekci Prostředky/Transakce se zobrazí transakce „Vrácení peněz z rozhodčího řízení“ -disputeSummaryWindow.close.closePeer=Potřebujete také zavřít ticket obchodního partnera! +disputeSummaryWindow.close.nextStepsForMediation=\nDalší kroky:\n\ +Otevřete obchod a přijměte nebo odmítněte návrhy od mediátora +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nDalší kroky:\n\ +Nevyžadují se od vás žádné další kroky. Pokud rozhodce rozhodl ve váš prospěch, v sekci Prostředky/Transakce se zobrazí transakce 'Vrácení peněz z rozhodčího řízení' +disputeSummaryWindow.close.closePeer=Potřebujete také zavřít žádost obchodního partnera! disputeSummaryWindow.close.txDetails.headline=Zveřejněte transakci vrácení peněz # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.buyer=Kupující obdrží {0} na adresu: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Prodejce obdrží {0} na adresu: {1}\n -disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3}\n\nOpravdu chcete tuto transakci zveřejnit? +disputeSummaryWindow.close.txDetails=Výdaje: {0}\n\ + {1} {2}\ + Transakční poplatek: {3}\n\n\ + Opravdu chcete tuto transakci zveřejnit? disputeSummaryWindow.close.noPayout.headline=Uzavřít bez jakékoli výplaty disputeSummaryWindow.close.noPayout.text=Chcete zavřít bez výplaty? +disputeSummaryWindow.close.alreadyPaid.headline=Výplata již proběhla +disputeSummaryWindow.close.alreadyPaid.text=Restartujte klienta pro provedení další výplaty u tohoto sporu + emptyWalletWindow.headline={0} nouzový nástroj peněženky -emptyWalletWindow.info=Použijte jej pouze v naléhavých případech, pokud nemůžete získat přístup k vašim prostředkům z uživatelského rozhraní.\n\nUpozorňujeme, že při použití tohoto nástroje budou všechny otevřené nabídky automaticky uzavřeny.\n\nPřed použitím tohoto nástroje si prosím zálohujte datový adresář. Můžete to udělat na obrazovce \"Účet/Záloha\".\n\nNahlaste nám svůj problém a nahlaste zprávu o chybě na GitHubu nebo na fóru Haveno, abychom mohli prozkoumat, co způsobilo problém. +emptyWalletWindow.info=Použijte jej pouze v naléhavých případech, pokud nemůžete získat přístup k vašim prostředkům z uživatelského rozhraní.\n\n\ +Upozorňujeme, že při použití tohoto nástroje budou všechny otevřené nabídky automaticky uzavřeny.\n\n\ +Před použitím tohoto nástroje si prosím zálohujte datový adresář. \ +Můžete to udělat na obrazovce \"Účet/Záloha\".\n\n\ +Nahlaste nám svůj problém a nahlaste nám chybu na GitHubu nebo na fóru Haveno, abychom mohli prozkoumat, co způsobilo problém. emptyWalletWindow.balance=Váš zůstatek v peněžence emptyWalletWindow.address=Vaše cílová adresa emptyWalletWindow.button=Pošlete všechny prostředky @@ -1415,19 +1941,19 @@ enterPrivKeyWindow.headline=Zadejte soukromý klíč pro registraci filterWindow.headline=Upravit seznam filtrů filterWindow.offers=Filtrované nabídky (oddělené čárkami) filterWindow.onions=Onion adresy vyloučené z obchodování (oddělené čárkami) -filterWindow.bannedFromNetwork=Onion adresy vyloučené ze síťové komunikace (oddělené čárkami) +filterWindow.bannedFromNetwork=Onion adresy blokované ze síťové komunikace (oddělené čárkami) filterWindow.accounts=Filtrovaná data obchodního účtu:\nFormát: seznam [ID platební metody | datové pole | hodnota] oddělený čárkami -filterWindow.bannedCurrencies=Filtrované kódy měn (oddělené čárkami) -filterWindow.bannedPaymentMethods=ID filtrované platební metody (oddělené čárkami) -filterWindow.bannedAccountWitnessSignerPubKeys=Filtrované veřejné klíče účtů podepisujícího svědka (hex nebo pub klíče oddělené čárkou) -filterWindow.bannedPrivilegedDevPubKeys=Filtrované privilegované klíče pub dev (hex nebo pub klíče oddělené čárkou) +filterWindow.bannedCurrencies=Blokované kódy měn (oddělené čárkami) +filterWindow.bannedPaymentMethods=ID blokované platební metody (oddělené čárkami) +filterWindow.bannedAccountWitnessSignerPubKeys=Blokované veřejné klíče účtů podepisujícího svědka (hex nebo pub klíče oddělené čárkou) +filterWindow.bannedPrivilegedDevPubKeys=Blokované privilegované klíče pub dev (hex nebo pub klíče oddělené čárkou) filterWindow.arbitrators=Filtrovaní rozhodci (onion adresy oddělené čárkami) filterWindow.mediators=Filtrovaní mediátoři (onion adresy oddělené čárkami) filterWindow.refundAgents=Filtrovaní rozhodci pro vrácení peněz (onion adresy oddělené čárkami) -filterWindow.seedNode=Filtrované seed nody (onion adresy oddělené čárkami) -filterWindow.priceRelayNode=Filtrované cenové relay nody (onion adresy oddělené čárkami) -filterWindow.xmrNode=Filtrované Moneroové nody (adresy+porty oddělené čárkami) -filterWindow.preventPublicXmrNetwork=Zabraňte použití veřejné moneroové sítě +filterWindow.seedNode=Filtrované seed uzly (onion adresy oddělené čárkami) +filterWindow.priceRelayNode=Filtrované cenové relay uzly (onion adresy oddělené čárkami) +filterWindow.xmrNode=Filtrované uzly Monero (adresy+porty oddělené čárkami) +filterWindow.preventPublicXmrNetwork=Zabraňte použití veřejné sítě Monero filterWindow.disableAutoConf=Zakázat automatické potvrzení filterWindow.autoConfExplorers=Filtrované průzkumníky s automatickým potvrzením (adresy oddělené čárkami) filterWindow.disableTradeBelowVersion=Min. verze nutná pro obchodování @@ -1441,24 +1967,33 @@ offerDetailsWindow.minXmrAmount=Min. částka XMR offerDetailsWindow.min=(min. {0}) offerDetailsWindow.distance=(vzdálenost od tržní ceny: {0}) offerDetailsWindow.myTradingAccount=Můj obchodní účet -offerDetailsWindow.offererBankId=(ID banky/BIC/SWIFT tvůrce) -offerDetailsWindow.offerersBankName=(název banky tvůrce) offerDetailsWindow.bankId=ID banky (např. BIC nebo SWIFT) 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: Umístit nabídku {0} monero -offerDetailsWindow.confirm.taker=Potvrďte: Využít nabídku {0} monero +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.creationDate=Datum vzniku offerDetailsWindow.makersOnion=Onion adresa tvůrce +offerDetailsWindow.challenge=Passphrase nabídky 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. qRCodeWindow.request=Žádost o platbu:\n{0} selectDepositTxWindow.headline=Vyberte vkladovou transakci ke sporu -selectDepositTxWindow.msg=Vkladová transakce nebyla v obchodě uložena.\nVyberte prosím jednu z existujících multisig transakcí z vaší peněženky, která byla vkladovou transakcí použitou při selhání obchodu.\n\nSprávnou transakci najdete tak, že otevřete okno s podrobnostmi o obchodu (klikněte na ID obchodu v seznamu) a sledujete výstup transakce s platebním poplatkem za obchodní transakci k následující transakci, kde uvidíte transakci s multisig vklady (adresa začíná na 3). Toto ID transakce by mělo být viditelné v seznamu zde prezentovaném. Jakmile najdete správnou transakci, vyberte ji a pokračujte.\n\nOmlouváme se za nepříjemnosti, ale tento případ chyby by se měl stát velmi zřídka a v budoucnu se pokusíme najít lepší způsoby, jak jej vyřešit. +selectDepositTxWindow.msg=Vkladová transakce nebyla v obchodě uložena.\n\ +Vyberte prosím jednu z existujících multisig transakcí z vaší peněženky, která byla \ +vkladovou transakcí použitou při selhání obchodu.\n\n\ +Správnou transakci najdete tak, že otevřete okno s podrobnostmi o obchodu (klikněte na ID obchodu v seznamu)\ + a sledujete výstup transakce s platebním poplatkem za obchodní transakci k následující transakci, \ +kde uvidíte multisig vkladovou transakci (adresa začíná na 3). Toto ID transakce by mělo být \ +viditelné v seznamu zde prezentovaném. Jakmile najdete správnou transakci, vyberte ji a pokračujte.\n\n\ +Omlouváme se za nepříjemnosti, ale tento případ chyby by se měl stát velmi zřídka \ +a v budoucnu se pokusíme najít lepší způsoby, jak jej vyřešit. selectDepositTxWindow.select=Vyberte vkladovou transakci sendAlertMessageWindow.headline=Odeslat globální oznámení @@ -1489,15 +2024,16 @@ setXMRTxKeyWindow.txKey=Transakční klíč (volitelný) tacWindow.headline=Uživatelská dohoda tacWindow.agree=Souhlasím tacWindow.disagree=Nesouhlasím a odcházím -tacWindow.arbitrationSystem=Řešení sporů +tacWindow.arbitrationSystem=Rozhodnutí sporu tradeDetailsWindow.headline=Obchod -tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce: +tradeDetailsWindow.disputedPayoutTxId=ID sporné platební transakce tradeDetailsWindow.tradeDate=Datum obchodu tradeDetailsWindow.txFee=Poplatek za těžbu tradeDetailsWindow.tradePeersOnion=Onion adresa obchodního partnera tradeDetailsWindow.tradePeersPubKeyHash=Pubkey hash obchodních partnerů tradeDetailsWindow.tradeState=Stav obchodu +tradeDetailsWindow.tradePhase=Fáze obchodu tradeDetailsWindow.agentAddresses=Rozhodce/Mediátor tradeDetailsWindow.detailData=Detailní data @@ -1507,6 +2043,7 @@ txDetailsWindow.xmr.noteReceived=Obdrželi jste XMR. txDetailsWindow.sentTo=Odesláno na txDetailsWindow.receivedWith=Přijato s txDetailsWindow.txId=TxId +txDetailsWindow.txKey=Klíč transakce closedTradesSummaryWindow.headline=Souhrn uzavřených obchodů closedTradesSummaryWindow.totalAmount.title=Celkový objem obchodů @@ -1518,6 +2055,9 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) walletPasswordWindow.headline=Pro odemknutí zadejte heslo +connectionFallback.headline=Chyba připojení k Moneru +connectionFallback.customNode=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? + torNetworkSettingWindow.header=Nastavení sítě Tor torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) torNetworkSettingWindow.providedBridges=Spojte se s poskytnutými mosty (bridges) @@ -1537,11 +2077,10 @@ torNetworkSettingWindow.deleteFiles.button=Odstranit zastaralé soubory Tor a vy 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).\nNavštivte webovou stránku Tor na adrese: https://bridges.torproject.org/bridges, kde se dozvíte více o mostech a připojitelných přepravách. +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, \ +kde se dozvíte více o mostech a pluggable transports. -feeOptionWindow.headline=Vyberte měnu pro platbu obchodního poplatku -feeOptionWindow.info=Můžete si vybrat, zda chcete zaplatit obchodní poplatek v BSQ nebo v XMR. Pokud zvolíte BSQ, oceníte zlevněný obchodní poplatek. -feeOptionWindow.optionsLabel=Vyberte měnu pro platbu obchodního poplatku feeOptionWindow.useXMR=Použít XMR feeOptionWindow.fee={0} (≈ {1}) feeOptionWindow.xmrFeeWithFiatAndPercentage={0} (≈ {1} / {2}) @@ -1565,30 +2104,46 @@ popup.headline.error=Chyba popup.doNotShowAgain=Znovu nezobrazovat popup.reportError.log=Otevřít log popup.reportError.gitHub=Nahlaste problém na GitHub -popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/bisq-network/bisq/issues.\nVýše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\nUsnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka „Otevřít log soubor“, uložením kopie a připojením ke zprávě o chybě. +popup.reportError={0}\n\nChcete-li nám pomoci vylepšit software, nahlaste tuto chybu otevřením nového problému na adrese https://github.com/haveno-dex/haveno/issues.\n\ +Výše uvedená chybová zpráva bude zkopírována do schránky po kliknutí na některé z níže uvedených tlačítek.\n\ +Usnadníte ladění, pokud zahrnete soubor haveno.log stisknutím tlačítka 'Otevřít log soubor', uložením kopie a připojením ke hlášení chyby. popup.error.tryRestart=Zkuste prosím restartovat aplikaci a zkontrolovat síťové připojení, abyste zjistili, zda můžete problém vyřešit. popup.error.takeOfferRequestFailed=Když se někdo pokusil využít jednu z vašich nabídek, došlo k chybě:\n{0} error.spvFileCorrupted=Při čtení řetězce SPV došlo k chybě.\nJe možné, že je poškozen řetězový soubor SPV.\n\nChybová zpráva: {0}\n\nChcete ji smazat a začít znovu synchronizovat? error.deleteAddressEntryListFailed=Soubor AddressEntryList nelze smazat.\nChyba: {0} -error.closedTradeWithUnconfirmedDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je stále nepotvrzená.\n\nProveďte prosím SPV resynchronizaci v \"Nastavení/Informace o síti\" a zkontrolujte, zda je transakce platná. -error.closedTradeWithNoDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je nulová.\n\nChcete-li vyčistit seznam uzavřených obchodů, restartujte aplikaci. +error.closedTradeWithUnconfirmedDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je stále \ + nepotvrzená.\n\n\ + Proveďte prosím SPV resynchronizaci v \"Nastavení/Informace o síti\" a zkontrolujte, zda je transakce platná. +error.closedTradeWithNoDepositTx=Vkladová transakce uzavřeného obchodu s obchodním ID {0} je nulová.\n\n\ + Chcete-li vyčistit seznam uzavřených obchodů, restartujte aplikaci. popup.warning.walletNotInitialized=Peněženka ještě není inicializována -popup.warning.osxKeyLoggerWarning=V souladu s přísnějšími bezpečnostními opatřeními v systému macOS 10.14 a novějších způsobí spuštění aplikace Java (Haveno používá Javu) upozornění na vyskakovací okno v systému MacOS („Haveno by chtěl přijímat stisknutí kláves z jakékoli aplikace“).\n\nChcete-li se tomuto problému vyhnout, otevřete své Nastavení macOS a přejděte do části "Zabezpečení a soukromí" -> "Soukromí" -> "Sledování vstupu" a ze seznamu na pravé straně odeberte „Haveno“.\n\nHaveno upgraduje na novější verzi Java, aby se tomuto problému vyhnul, jakmile budou vyřešena technická omezení (balíček Java Packager pro požadovanou verzi Java ještě není dodán). -popup.warning.wrongVersion=Pravděpodobně máte nesprávnou verzi Haveno pro tento počítač.\nArchitektura vašeho počítače je: {0}.\nBinární kód Haveno, který jste nainstalovali, je: {1}.\nVypněte prosím a znovu nainstalujte správnou verzi ({2}). -popup.warning.incompatibleDB=Zjistili jsme nekompatibilní soubory databáze!\n\nTyto databázové soubory nejsou kompatibilní s naší aktuální kódovou základnou:\n{0}\n\nVytvořili jsme zálohu poškozených souborů a aplikovali jsme výchozí hodnoty na novou verzi databáze.\n\nZáloha se nachází na adrese:\n{1}/db/backup_of_corrupted_data.\n\nZkontrolujte, zda máte nainstalovanou nejnovější verzi Haveno.\nMůžete si jej stáhnout na adrese: [HYPERLINK:https://haveno.exchange/downloads].\n\nRestartujte aplikaci. -popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete spustit dvě instance Haveno. +popup.warning.wrongVersion=Pravděpodobně máte nesprávnou verzi Haveno pro tento počítač.\n\ +Architektura vašeho počítače je: {0}.\n\ +Binární kód Haveno, který jste nainstalovali, je: {1}.\n\ +Vypněte prosím a znovu nainstalujte správnou verzi ({2}). +popup.warning.incompatibleDB=Zjistili jsme nekompatibilní soubory databáze!\n\n\ +Tyto databázové soubory nejsou kompatibilní s naší aktuální kódovou základnou:\n{0}\n\n\ +Vytvořili jsme zálohu poškozených souborů a aplikovali jsme výchozí hodnoty na novou verzi databáze.\n\n\ +Záloha se nachází na adrese:\n\ +{1}/db/backup_of_corrupted_data.\n\n\ +Zkontrolujte, zda máte nainstalovanou nejnovější verzi Haveno.\n\ +Můžete si jej stáhnout na adrese: [HYPERLINK:https://haveno.exchange/downloads].\n\n\ +Restartujte prosím aplikaci. +popup.warning.startupFailed.twoInstances=Haveno již běží. Nemůžete mít spuštěné dvě instance Haveno. popup.warning.tradePeriod.halfReached=Váš obchod s ID {0} dosáhl poloviny max. povoleného obchodního období a stále není dokončen.\n\nObdobí obchodování končí {1}\n\nDalší informace o stavu obchodu naleznete na adrese \"Portfolio/Otevřené obchody\". -popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\nObdobí obchodování skončilo {1}\n\nZkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. +popup.warning.tradePeriod.ended=Váš obchod s ID {0} dosáhl max. povoleného obchodního období a není dokončen.\n\n\ + Období obchodování skončilo {1}\n\n\ + Zkontrolujte prosím svůj obchod v sekci "Portfolio/Otevřené obchody\", abyste kontaktovali mediátora. popup.warning.noTradingAccountSetup.headline=Nemáte nastaven obchodní účet -popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo cryptoový účet.\nChcete si založit účet? +popup.warning.noTradingAccountSetup.msg=Než budete moci vytvořit nabídku, musíte si nastavit národní měnu nebo kryptoměnový účet.\nChcete si založit účet? popup.warning.noArbitratorsAvailable=Nejsou k dispozici žádní rozhodci. popup.warning.noMediatorsAvailable=Nejsou k dispozici žádní mediátoři. popup.warning.notFullyConnected=Musíte počkat, až budete plně připojeni k síti.\nTo může při spuštění trvat až 2 minuty. -popup.warning.notSufficientConnectionsToXmrNetwork=Musíte počkat, až budete mít alespoň {0} připojení k moneroové síti. -popup.warning.downloadNotComplete=Musíte počkat, až bude stahování chybějících moneroových bloků kompletní. +popup.warning.notSufficientConnectionsToXmrNetwork=Musíte počkat, až budete mít alespoň {0} připojení k síti Monero. +popup.warning.downloadNotComplete=Musíte počkat, až bude dokončeno stahování chybějících bloků Monero. popup.warning.walletNotSynced=Haveno peněženka není synchronizována s nejnovější výškou blockchainu. Počkejte, dokud se peněženka nesynchronizuje, nebo zkontrolujte své připojení. popup.warning.removeOffer=Opravdu chcete tuto nabídku odebrat? popup.warning.tooLargePercentageValue=Nelze nastavit procento 100% nebo větší. @@ -1596,48 +2151,94 @@ popup.warning.examplePercentageValue=Zadejte procento jako číslo \"5.4\" pro 5 popup.warning.noPriceFeedAvailable=Pro tuto měnu není k dispozici žádný zdroj cen. Nelze použít procentuální cenu.\nVyberte pevnou cenu. popup.warning.sendMsgFailed=Odeslání zprávy vašemu obchodnímu partnerovi se nezdařilo.\nZkuste to prosím znovu a pokud to i nadále selže, nahlaste chybu. popup.warning.messageTooLong=Vaše zpráva překračuje max. povolená velikost. Zašlete jej prosím v několika částech nebo ji nahrajte do služby, jako je https://pastebin.com. -popup.warning.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného obchodu.\nUzamčený zůstatek: {0}\nVkladová tx adresa: {1}\nObchodní ID: {2}.\n\nOtevř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.lockedUpFunds=Zamkli jste finanční prostředky z neúspěšného obchodu.\n\ + Uzamčený zůstatek: {0}\n\ + 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.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n +popup.warning.makerTxInvalid=Tato nabídka není platná. Prosím vyberte jinou nabídku.\n\n takeOffer.cancelButton=Zrušit akceptaci nabídky takeOffer.warningButton=Ignorovat a přesto pokračovat # suppress inspection "UnusedProperty" -popup.warning.nodeBanned=Jeden z {0} uzlů byl zabanován. +popup.warning.nodeBanned=Jeden z {0} uzlů byl zablokován. # suppress inspection "UnusedProperty" popup.warning.priceRelay=cenové relé popup.warning.seed=seed -popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. Další informace naleznete na fóru Haveno. -popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. +popup.warning.mandatoryUpdate.trading=Aktualizujte prosím na nejnovější verzi Haveno. \ + Byla vydána povinná aktualizace, která zakazuje obchodování se starými verzemi. \ + Další informace naleznete na fóru Haveno. +popup.warning.noFilter=Nepřijali jsme objekt filtru od seedových uzlů. Prosím informujte správce sítě, aby zaregistrovali objekt filtru. +popup.warning.burnXMR=Tato transakce není možná, protože poplatky za těžbu {0} by přesáhly částku převodu {1}. \ + Počkejte prosím, dokud nebudou poplatky za těžbu opět nízké nebo dokud nenahromadíte více XMR k převodu. -popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl moneroovou sítí odmítnut.\nID transakce = {1}.\nNabídka byla odstraněna, aby se předešlo dalším problémům.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.openOffer.makerFeeTxRejected=Transakční poplatek tvůrce za nabídku s ID {0} byl odmítnut sítí Monero.\n\ + ID transakce = {1}.\n\ + Nabídka byla odstraněna, aby se předešlo dalším problémům.\n\ + Přejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. popup.warning.trade.txRejected.tradeFee=obchodní poplatek popup.warning.trade.txRejected.deposit=vklad -popup.warning.trade.txRejected=Moneroová síť odmítla {0} transakci pro obchod s ID {1}.\nID transakce = {2}\nObchod byl přesunut do neúspěšných obchodů.\nPřejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.trade.txRejected=Síť Monero odmítla {0} transakci pro obchod s ID {1}.\n\ + ID transakce = {2}\n\ + Obchod byl přesunut do neúspěšných obchodů.\n\ + Přejděte do části \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\nID transakce = {1}.\nPřejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\nPro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. +popup.warning.openOfferWithInvalidMakerFeeTx=Transakční poplatek tvůrce za nabídku s ID {0} je neplatný.\n\ + ID transakce = {1}.\n\ + Přejděte do \"Nastavení/Informace o síti\" a proveďte synchronizaci SPV.\n\ + Pro další pomoc prosím kontaktujte podpůrný kanál v Haveno Keybase týmu. -popup.info.securityDepositInfo=Aby oba obchodníci dodržovali obchodní protokol, musí oba obchodníci zaplatit kauci.\n\nTento vklad je uložen ve vaší obchodní peněžence, dokud nebude váš obchod úspěšně dokončen a poté vám bude vrácen.\n\nPoznámka: Pokud vytváříte novou nabídku, musí program Haveno běžet, aby ji převzal jiný obchodník. Chcete-li zachovat své nabídky online, udržujte Haveno spuštěný a ujistěte se, že tento počítač zůstává online (tj. Zkontrolujte, zda se nepřepne do pohotovostního režimu...pohotovostní režim monitoru je v pořádku). - -popup.info.cashDepositInfo=Ujistěte se, že ve své oblasti máte pobočku banky, abyste mohli provést hotovostní vklad.\nID banky prodávajícího (BIC/SWIFT) je: {0}. +popup.info.cashDepositInfo=Ujistěte se, že ve své oblasti máte pobočku banky, abyste mohli provést hotovostní vklad.\n\ + ID banky prodávajícího (BIC/SWIFT) je: {0}. popup.info.cashDepositInfo.confirm=Potvrzuji, že mohu provést vklad -popup.info.shutDownWithOpenOffers=Haveno se vypíná, ale existují otevřené nabídky.\n\nTyto nabídky nebudou dostupné v síti P2P, pokud bude Haveno vypnutý, ale budou znovu publikovány do sítě P2P při příštím spuštění Haveno.\n\nChcete-li zachovat své nabídky online, udržujte Haveno spuštěný a ujistěte se, že tento počítač zůstává online (tj. Ujistěte se, že nepřejde do pohotovostního režimu...pohotovostní režim monitoru není problém). -popup.info.qubesOSSetupInfo=Zdá se, že používáte Haveno na Qubes OS.\n\nUjistěte se, že je vaše Haveno qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. +popup.info.shutDownWithOpenOffers=Haveno se vypíná, ale existují otevřené nabídky.\n\n\ + Tyto nabídky nebudou dostupné v síti P2P, pokud bude Haveno vypnutý, ale budou znovu \ + zveřejněny do sítě P2P při příštím spuštění Haveno. Chcete-li zachovat své nabídky \ + online, udržujte Haveno spuštěné a připojení k internetu (ujistěte se, že počítač \ +nepřejde do pohotovostního režimu... pohotovostní režim monitoru není problém). +popup.info.shutDownWithTradeInit={0}\n\ + Tento obchod se ještě neinicializoval; jeho ukončení by pravděpodobně vedlo k jeho poškození. Počkejte prosím minutu a zkuste to znovu. +popup.info.shutDownWithDisputeInit=Služba Haveno se vypíná, ale stále je zde čekající zpráva systému sporu.\n\ + Před vypnutím prosím počkejte minutu. +popup.info.shutDownQuery=Jste si jisti, že chcete opustit Haveno? +popup.info.qubesOSSetupInfo=Zdá se, že používáte Haveno na Qubes OS.\n\n\ + Ujistěte se, že je vaše Haveno qube nastaveno podle našeho průvodce nastavením na [HYPERLINK:https://haveno.exchange/wiki/Running_Haveno_on_Qubes]. +popup.info.p2pStatusIndicator.red={0}\n\n\ + Váš uzel není připojen k síti P2P. V tomto stavu nemůže Haveno fungovat. +popup.info.p2pStatusIndicator.yellow={0}\n\n\ + Váš uzel nemá žádná příchozí připojení Tor. Haveno bude fungovat v pořádku, ale pokud tento stav přetrvává několik hodin, může to znamenat problémy s připojením. +popup.info.p2pStatusIndicator.green={0}\n\n\ + Dobrá zpráva, stav vašeho připojení P2P vypadá zdravě! +popup.info.firewallSetupInfo=Zdá se, že tento počítač blokuje příchozí připojení Tor. \ + K tomu může dojít v prostředích virtuálních počítačů, jako je Qubes/VirtualBox/Whonix. \n\n\ + Nastavte prosím své prostředí tak, aby přijímalo příchozí připojení Tor, jinak vaše nabídky nebude moci nikdo přijmout. popup.warn.downGradePrevention=Downgrade z verze {0} na verzi {1} není podporován. Použijte prosím nejnovější verzi Haveno. +popup.warn.daoRequiresRestart=Vyskytl se problém se synchronizací stavu DAO. Pro odstranění problému je nutné restartovat aplikaci. popup.privateNotification.headline=Důležité soukromé oznámení! -popup.securityRecommendation.headline=Důležité bezpečnostní doporučení -popup.securityRecommendation.msg=Chtěli bychom vám připomenout, abyste zvážili použití ochrany heslem pro vaši peněženku, pokud jste ji již neaktivovali.\n\nDůrazně se také doporučuje zapsat seed slova peněženky. Tato seed slova jsou jako hlavní heslo pro obnovení vaší moneroové peněženky.\nV sekci "Seed peněženky" naleznete další informace.\n\nDále byste měli zálohovat úplnou složku dat aplikace v sekci \"Záloha\". - -popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero node.\n\nUjistěte se, prosím, že tento node je plně synchronizován před spuštěním Havena. - +popup.xmrLocalNode.msg=Haveno zjistil, že na tomto stroji (na localhostu) běží Monero uzel.\n\n\Prosím ujistěte se, že tento uzel je plně synchronizován před spuštěním Havena. popup.shutDownInProgress.headline=Probíhá vypínání popup.shutDownInProgress.msg=Vypnutí aplikace může trvat několik sekund.\nProsím, nepřerušujte tento proces. -popup.attention.forTradeWithId=Je třeba věnovat pozornost obchodu s ID {0} -popup.attention.reasonForPaymentRuleChange=Verze 1.5.5 přináší zásadní změnu v pravidlech obchodování ohledně \"důvodu platby\" v bankovních převodech. Prosím nechte toto pole prázdné -- ID obchodu již v poli \"důvod platby\" NEPOUŽÍVEJTE. +popup.attention.forTradeWithId=Je třeba věnovat pozornost obchodu ID {0} +popup.attention.welcome.stagenet=Vítejte v testovací instanci Haveno!\n\n\ +Tato platforma umožňuje testovat protokol Haveno. Ujistěte se, že postupujete podle pokynů [HYPERLINK:https://github.com/haveno-dex/haveno/blob/master/docs/installing.md].\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ +Jedná se o testovací instanci. Nepoužívejte skutečné peníze! +popup.attention.welcome.mainnet=Vítejte v Haveno!\n\n\ +Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ +Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new]. +popup.attention.welcome.mainnet.test=Vítejte v Haveno!\n\n\ +Tato platforma umožňuje decentralizovaně obchodovat s měnou Monero za fiat měny nebo jiné kryptoměny.\n\n\ +Začněte tím, že si vytvoříte nový platební účet a poté vytvoříte nebo přijmete nabídku.\n\n\ +Pokud narazíte na nějaký problém, dejte nám prosím vědět [HYPERLINK:https://github.com/haveno-dex/haveno/issues/new].\n\n\ +Systém Haveno byl nedávno uvolněn k veřejnému testování. Používejte prosím nízké částky! popup.info.multiplePaymentAccounts.headline=K dispozici jsou účty a více platebními metodami popup.info.multiplePaymentAccounts.msg=Pro tuto nabídku máte k dispozici více platebních účtů. Ujistěte se, že jste vybrali ten správný. @@ -1658,11 +2259,13 @@ popup.accountSigning.signAccounts.ECKey.error=Špatný ECKey rozhodce popup.accountSigning.success.headline=Gratulujeme popup.accountSigning.success.description=Všechny {0} platební účty byly úspěšně podepsány! -popup.accountSigning.generalInformation=Podpisový stav všech vašich účtů najdete v sekci účtu.\n\nDalší informace naleznete na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. +popup.accountSigning.generalInformation=Podpisový stav všech vašich účtů najdete v sekci účtu.\n\n + Další informace naleznete na adrese [HYPERLINK:https://docs.haveno.exchange/payment-methods#account-signing]. popup.accountSigning.signedByArbitrator=Jeden z vašich platebních účtů byl ověřen a podepsán rozhodcem. Obchodování s tímto účtem po úspěšném obchodování automaticky podepíše účet vašeho obchodního partnera.\n\n{0} popup.accountSigning.signedByPeer=Jeden z vašich platebních účtů byl ověřen a podepsán obchodním partnerem. Váš počáteční obchodní limit bude zrušen a do {0} dnů budete moci podepsat další účty.\n\n{1} popup.accountSigning.peerLimitLifted=Počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} -popup.accountSigning.peerSigner=Jeden z vašich účtů je dostatečně zralý, aby podepsal další platební účty, a počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} +popup.accountSigning.peerSigner=Jeden z vašich účtů je dostatečně zralý, aby podepsal další platební účty, \ + a počáteční limit pro jeden z vašich účtů byl zrušen.\n\n{0} popup.accountSigning.singleAccountSelect.headline=Importujte svědka stáří účtu k podepsání popup.accountSigning.confirmSingleAccount.headline=Potvrďte vybrané svědky o stáří účtu @@ -1677,12 +2280,22 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys byly podepsány popup.accountSigning.unsignedPubKeys.result.signed=Podepsané pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Nepodařilo se podepsat +popup.info.buyerAsTakerWithoutDeposit.headline=Žádný vklad není od kupujícího požadován +popup.info.buyerAsTakerWithoutDeposit=\ + Vaše nabídka nebude vyžadovat bezpečnostní zálohu ani poplatek od kupujícího XMR.\n\n + Pro přijetí vaší nabídky musíte sdílet heslo se svým obchodním partnerem mimo Haveno.\n\n\ + Heslo je automaticky vygenerováno a zobrazeno v detailech nabídky po jejím vytvoření.\ + +popup.info.torMigration.msg=Váš uzel Haveno pravděpodobně používá zastaralou adresu Tor v2. \ + Přepněte svůj uzel Haveno na adresu Tor v3. \ + Nezapomeňte si předem zálohovat datový adresář. + #################################################################### # Notifications #################################################################### notification.trade.headline=Oznámení o obchodu s ID {0} -notification.ticket.headline=Úkol na podporu pro obchod s ID {0} +notification.ticket.headline=Úkol pro podporu pro obchod s ID {0} notification.trade.completed=Obchod je nyní dokončen a můžete si vybrat své prostředky. notification.trade.accepted=Vaše nabídka byla přijata XMR {0}. notification.trade.unlocked=Váš obchod má alespoň jedno potvrzení blockchainu.\nPlatbu můžete začít hned teď. @@ -1705,7 +2318,7 @@ systemTray.show=Otevřít okno aplikace systemTray.hide=Skrýt okno aplikace systemTray.info=Informace o Haveno systemTray.exit=Odejít -systemTray.tooltip=Haveno: Decentralizovaná směnárna moneroů +systemTray.tooltip=Haveno: Decentralizovaná směnárna monero #################################################################### @@ -1723,7 +2336,12 @@ guiUtil.accountExport.exportFailed=Export do CSV selhal kvůli chybě.\nChyba = guiUtil.accountExport.selectExportPath=Vyberte složku pro export guiUtil.accountImport.imported=Obchodní účet importovaný z:\n{0}\n\nImportované účty:\n{1} guiUtil.accountImport.noAccountsFound=Nebyly nalezeny žádné exportované obchodní účty na: {0}.\nNázev souboru je {1}." -guiUtil.openWebBrowser.warning=Chystáte se otevřít webovou stránku ve webovém prohlížeči.\nChcete nyní otevřít webovou stránku?\n\nPokud nepoužíváte \"Tor Browser\" jako výchozí systémový webový prohlížeč, připojíte se k webové stránce přes nechráněné spojení.\n\nURL: \"{0}\" +guiUtil.openWebBrowser.warning=Chystáte se otevřít webovou stránku \ +ve webovém prohlížeči.\n\ +Chcete nyní otevřít webovou stránku?\n\n\ +Pokud nepoužíváte \"Tor Browser\" jako výchozí systémový webový prohlížeč, \ +připojíte se k webové stránce přes nechráněné spojení.\n\n\ +URL: \"{0}\" guiUtil.openWebBrowser.doOpen=Otevřít webovou stránku a znovu se neptat guiUtil.openWebBrowser.copyUrl=Zkopírovat URL a zrušit guiUtil.ofTradeAmount=obchodní částky @@ -1748,11 +2366,12 @@ peerInfoIcon.tooltip.trade.traded={0} onion adresa: {1}\nUž jste s tímto partn peerInfoIcon.tooltip.trade.notTraded={0} onion adresa: {1}\nDosud jste s tímto partnerem neobchodovali.\n{2} peerInfoIcon.tooltip.age=Platební účet byl vytvořen před {0}. peerInfoIcon.tooltip.unknownAge=Stáří platebního účtu není znám. +peerInfoIcon.tooltip.dispute={0}nPočet sporů: {1}.\n{2} tooltip.openPopupForDetails=Otevřít vyskakovací okno s podrobnostmi tooltip.invalidTradeState.warning=Tento obchod je v neplatném stavu. Chcete-li získat další informace, otevřete okno s podrobnostmi -tooltip.openBlockchainForAddress=Otevřít externí blockchain explorer pro adresu: {0} -tooltip.openBlockchainForTx=Otevřete externí blockchain explorer pro transakci: {0} +tooltip.openBlockchainForAddress=Otevřít externí průzkumník blockchainu pro adresu: {0} +tooltip.openBlockchainForTx=Otevřít externí průzkumník blockchainu pro transakci: {0} confidence.unknown=Neznámý stav transakce confidence.seen=Viděno {0} partnery / 0 potvrzení @@ -1767,10 +2386,14 @@ peerInfo.age.noRisk=Stáří platebního účtu peerInfo.age.chargeBackRisk=Čas od podpisu peerInfo.unknownAge=Stáří není známo -addressTextField.openWallet=Otevřete výchozí moneroovou peněženku +addressTextField.openWallet=Otevřete výchozí peněženku Monero addressTextField.copyToClipboard=Zkopírujte adresu do schránky addressTextField.addressCopiedToClipboard=Adresa byla zkopírována do schránky -addressTextField.openWallet.failed=Otevření výchozí moneroové peněženky se nezdařilo. Možná nemáte žádnou nainstalovanou? +addressTextField.openWallet.failed=Otevření výchozí peněženky Monero selhalo. Možná nemáte žádnou nainstalovanou? + +explorerAddressTextField.copyToClipboard=Kopírovat adresu do schránky +explorerAddressTextField.blockExplorerIcon.tooltip=Otevřete průzkumník blockchainu s touto adresou +explorerAddressTextField.missingTx.warning.tooltip=Chybí požadovaná adresa peerInfoIcon.tooltip={0}\nŠtítek: {1} @@ -1785,11 +2408,11 @@ txIdTextField.missingTx.warning.tooltip=Chybí požadovaná transakce navigation.account=\"Účet\" navigation.account.walletSeed=\"Účet/Seed peněženky\" -navigation.funds.availableForWithdrawal=\"Prostředky/Odeslat prostředky\" +navigation.funds.availableForWithdrawal=\"Prostředky/Poslat finanční prostředky\" navigation.portfolio.myOpenOffers=\"Portfolio/Moje otevřené nabídky\" navigation.portfolio.pending=\"Portfolio/Otevřené obchody\" navigation.portfolio.closedTrades=\"Portfolio/Historie\" -navigation.funds.depositFunds=\"Prostředky/Přijmout prostředky\" +navigation.funds.depositFunds=\"Prostředky/Přijmout finanční prostředky\" navigation.settings.preferences=\"Nastavení/Preference\" # suppress inspection "UnusedProperty" navigation.funds.transactions=\"Prostředky/Transakce\" @@ -1802,11 +2425,14 @@ navigation.support=\"Podpora\" formatter.formatVolumeLabel={0} částka{1} formatter.makerTaker=Tvůrce jako {0} {1} / Příjemce jako {2} {3} +formatter.makerTaker.locked=Tvůrce jako {0} {1} / Příjemce jako {2} {3} 🔒 formatter.youAreAsMaker=Jste {1} {0} (jako tvůrce) / Příjemce je {3} {2} formatter.youAreAsTaker=Jste {1} {0} (jako příjemce) / Tvůrce je {3} {2} formatter.youAre={0}te {1} ({2}te {3}) formatter.youAreCreatingAnOffer.traditional=Vytváříte nabídku: {0} {1} -formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2}te {3}) +formatter.youAreCreatingAnOffer.traditional.locked=Vytváříte nabídku: {0} {1} 🔒 +formatter.youAreCreatingAnOffer.crypto=Vytváříte nabídku: {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Vytváříte nabídku: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} jako tvůrce formatter.asTaker={0} {1} jako příjemce @@ -1826,18 +2452,20 @@ XMR_STAGENET=Monero Stagenet time.year=Rok time.month=Měsíc +time.halfYear=Půlrok +time.quarter=Čtvrtrok time.week=Týden time.day=Den time.hour=Hodina time.minute10=10 minut -time.hours=hodiny -time.days=dny +time.hours=hodin +time.days=dnů time.1hour=1 hodina time.1day=1 den time.minute=minuta time.second=sekunda -time.minutes=minuty -time.seconds=sekundy +time.minutes=minut +time.seconds=sekund password.enterPassword=Vložte heslo @@ -1847,11 +2475,12 @@ password.deriveKey=Odvozuji klíč z hesla password.walletDecrypted=Peněženka úspěšně dešifrována a ochrana heslem byla odstraněna. password.wrongPw=Zadali jste nesprávné heslo.\n\nZkuste prosím zadat heslo znovu a pečlivě zkontrolujte překlepy nebo pravopisné chyby. password.walletEncrypted=Peněženka úspěšně šifrována a ochrana heslem povolena. +password.walletEncryptionFailed=Heslo se nepodařilo nastavit password.passwordsDoNotMatch=Zadaná 2 hesla se neshodují. password.forgotPassword=Zapomněli jste heslo? -password.backupReminder=Pamatujte, že při nastavování hesla do peněženky budou odstraněny všechny automaticky vytvořené zálohy z nezašifrované peněženky.\n\nPřed nastavením hesla se důrazně doporučuje provést zálohu adresáře aplikace a zapsat si počáteční slova! -password.backupWasDone=Už jsem provedl zálohu -password.setPassword=Nastavit heslo (Už jsem provedl zálohu) +password.backupReminder=Pamatujte, že při nastavování hesla do peněženky budou smazány všechny automaticky vytvořené zálohy z nezašifrované peněženky.\n\n\ + Před nastavením hesla se důrazně doporučuje provést zálohu adresáře aplikace a zapsat si seed slova! +password.setPassword=Nastavit heslo (Už jsem provedl/a zálohu) password.makeBackup=Provést zálohu seed.seedWords=Seed slova peněženky @@ -1860,14 +2489,25 @@ seed.date=Datum peněženky seed.restore.title=Obnovit peněženky z seed slov seed.restore=Obnovit peněženky seed.creationDate=Datum vzniku -seed.warn.walletNotEmpty.msg=Vaše moneroová peněženka není prázdná.\n\nTuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek může vést ke zneplatnění záloh.\n\nDokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své moneroy.\nV případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\nNouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". +seed.warn.walletNotEmpty.msg=Vaše peněženka Monero není prázdná.\n\n\ +Tuto peněženku musíte vyprázdnit, než se pokusíte obnovit starší, protože smíchání peněženek \ +může vést ke zneplatnění záloh.\n\n\ +Dokončete své obchody, uzavřete všechny otevřené nabídky a přejděte do sekce Prostředky, kde si můžete vybrat své monero.\n\ +V případě, že nemáte přístup ke svým moneroům, můžete použít nouzový nástroj k vyprázdnění peněženky.\n\ +Nouzový nástroj otevřete stisknutím kombinace kláves \"Alt+e\" nebo \"Cmd/Ctrl+e\". seed.warn.walletNotEmpty.restore=Chci přesto obnovit seed.warn.walletNotEmpty.emptyWallet=Nejprve vyprázdním své peněženky -seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\nPo obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\nChcete pokračovat? -seed.warn.walletDateEmpty=Protože jste nezadali datum peněženky, bude muset haveno skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\nPeněženky BIP39 byly poprvé představeny v haveno dne 2017.06.28 (verze v0.5). Tímto datem můžete ušetřit čas.\n\nV ideálním případě byste měli určit datum, kdy byl vytvořen váš seed peněženky.\n\n\nOpravdu chcete pokračovat bez zadání data peněženky? +seed.warn.notEncryptedAnymore=Vaše peněženky jsou šifrovány.\n\n\ +Po obnovení již nebudou peněženky šifrovány a musíte nastavit nové heslo.\n\n\ +Chcete pokračovat? +seed.warn.walletDateEmpty=Protože jste nezadali datum peněženky, bude muset haveno skenovat blockchain od roku 2013.10.09 (datum spuštění BIP39).\n\n\ +Peněženky BIP39 byly poprvé představeny v Haveno dne 2017.06.28 (verze v0.5). Použitím tohoto data můžete ušetřit čas.\n\n\ +V ideálním případě byste měli určit datum, kdy byl vytvořen váš seed peněženky.\n\n\n\ +Opravdu chcete pokračovat bez zadání data peněženky? seed.restore.success=Peněženky byly úspěšně obnoveny pomocí nových seed slov.\n\nMusíte vypnout a restartovat aplikaci. seed.restore.error=Při obnově peněženek pomocí seed slov došlo k chybě. {0} -seed.restore.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud obnovíte ze seedu.\nJste si jisti, že chcete pokračovat? +seed.restore.openOffers.warn=Máte otevřené nabídky, které budou odstraněny, pokud obnovíte ze seedu.\n\ + Jste si jisti, že chcete pokračovat? #################################################################### @@ -1883,9 +2523,10 @@ payment.account.owner=Celé jméno vlastníka účtu payment.account.fullName=Celé jméno (křestní, střední, příjmení) payment.account.state=Stát/Provincie/Region payment.account.city=Město +payment.account.address=Adresa payment.bank.country=Země původu banky -payment.account.name.email=Celé jméno / e-mail majitele účtu -payment.account.name.emailAndHolderId=Celé jméno / e-mail / majitele účtu {0} +payment.account.name.email=Celé jméno majitele účtu / e-mail +payment.account.name.emailAndHolderId=Celé jméno majitele účtu / e-mail / {0} payment.bank.name=Jméno banky payment.select.account=Vyberte typ účtu payment.select.region=Vyberte region @@ -1897,14 +2538,58 @@ payment.email=E-mail payment.country=Země payment.extras=Zvláštní požadavky payment.email.mobile=E-mail nebo mobilní číslo -payment.crypto.address=Crypto adresa -payment.crypto.tradeInstantCheckbox=Obchodujte ihned s tímto cryptoem (do 1 hodiny) -payment.crypto.tradeInstant.popup=Pro okamžité obchodování je nutné, aby oba obchodní partneři byli online, aby mohli obchod dokončit za méně než 1 hodinu.\n\nPokud máte otevřené nabídky a nejste k dispozici, deaktivujte je na obrazovce „Portfolio“. -payment.crypto=Crypto -payment.select.crypto=Vyberte nebo vyhledejte crypto +payment.email.mobile.cashtag=Cashtag, e-mail, nebo mobilní číslo. +payment.email.mobile.username=Uživatelské jméno, e-mail, nebo mobilní číslo. +payment.crypto.address=Adresa kryptoměny +payment.crypto.tradeInstantCheckbox=Obchodujte okamžitě (do 1 hodiny) s touto kryptoměnou +payment.crypto.tradeInstant.popup=Pro okamžité obchodování je nutné, aby oba obchodní partneři byli online, aby mohli \ + obchod dokončit za méně než 1 hodinu.\n\n\ + Pokud máte otevřené nabídky a nejste k dispozici, \ + deaktivujte je na obrazovce 'Portfolio'. +payment.crypto=Kryptoměna +payment.select.crypto=Vyberte nebo vyhledejte kryptoměnu payment.secret=Tajná otázka payment.answer=Odpověď payment.wallet=ID peněženky +payment.capitual.cap=CAP kód +payment.upi.virtualPaymentAddress=Virtuální platební adresa + +# suppress inspection "UnusedProperty" +payment.swift.headline=Mezinárodní SWIFT Wire Transfer +# suppress inspection "UnusedProperty" +payment.swift.title.bank=Přijímající banka +# suppress inspection "UnusedProperty" +payment.swift.title.intermediary=Zprostředkující banka (klikněte pro rozbalení) +# suppress inspection "UnusedProperty" +payment.swift.country.bank=Země přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.country.intermediary=Země zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.bank=SWIFT kód přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.swiftCode.intermediary=SWIFT kód zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.name.bank=Název přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.name.intermediary=Název zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.branch.bank=Pobočka přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.branch.intermediary=Pobočka zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.address.bank=Adresa přijímající banky +# suppress inspection "UnusedProperty" +payment.swift.address.intermediary=Adresa zprostředkující banky +# suppress inspection "UnusedProperty" +payment.swift.address.beneficiary=Adresa příjemce +# suppress inspection "UnusedProperty" +payment.swift.phone.beneficiary=Telefon příjemce prostředků +payment.swift.account=Číslo účtu (nebo IBAN) +payment.swift.use.intermediary=Použít zprostředkující banku +payment.swift.showPaymentInfo=Ukázat platební informace... +payment.account.owner.address=Adresa majitele účtu +payment.transferwiseUsd.address=(musí být v US, zvažte použití adresy banky) + payment.amazon.site=Kupte Amazon eGift zde: payment.ask=Zjistěte pomocí obchodního chatu payment.uphold.accountId=Uživatelské jméno, e-mail nebo číslo telefonu @@ -1915,7 +2600,9 @@ payment.supportedCurrencies=Podporované měny payment.supportedCurrenciesForReceiver=Měny pro příjem prostředků payment.limitations=Omezení payment.salt=Salt pro ověření stáří účtu -payment.error.noHexSalt=Salt musí být ve formátu HEX.\nDoporučujeme upravit pole salt, pokud chcete salt převést ze starého účtu, aby bylo stáří vašeho účtu zachováno. Stáří účtu se ověřuje pomocí salt účtu a identifikačních údajů účtu (např. IBAN). +payment.error.noHexSalt=Salt musí být ve formátu HEX.\n\ + Doporučujeme upravit pole salt, pokud chcete salt převést ze starého účtu, aby bylo stáří vašeho účtu zachováno. \ + Stáří účtu se ověřuje pomocí salt účtu a identifikačních údajů účtu (např. IBAN). payment.accept.euro=Přijímejte obchody z těchto zemí eurozóny payment.accept.nonEuro=Přijímejte obchody z těchto zemí mimo eurozónu payment.accepted.countries=Akceptované země @@ -1927,7 +2614,7 @@ shared.accountSigningState=Stav podpisu účtu #new payment.crypto.address.dyn={0} adresa -payment.crypto.receiver.address=Cryptoová adresa příjemce +payment.crypto.receiver.address=Kryptoměnová adresa příjemce payment.accountNr=Číslo účtu payment.emailOrMobile=E-mail nebo mobilní číslo payment.useCustomAccountName=Použijte vlastní název účtu @@ -1945,47 +2632,424 @@ payment.bankIdOptional=ID Banky (BIC/SWIFT) (volitelné) payment.branchNr=Číslo pobočky payment.branchNrOptional=Číslo pobočky (volitelné) payment.accountNrLabel=Číslo účtu (IBAN) +payment.iban=IBAN +payment.tikkie.iban=IBAN použitý pro obchodování Haveno na Tikkie payment.accountType=Typ účtu payment.checking=Kontrola payment.savings=Úspory payment.personalId=Číslo občanského průkazu -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.zelle.info=Zelle je služba převodu peněz, která funguje nejlépe *prostřednictvím* jiné banky.\n\n1. Na této stránce zjistěte, zda (a jak) vaše banka spolupracuje se Zelle:\n[HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Zaznamenejte si zvláštní limity převodů - limity odesílání se liší podle banky a banky často určují samostatné denní, týdenní a měsíční limity.\n\n3. Pokud vaše banka s Zelle nepracuje, můžete ji stále používat prostřednictvím mobilní aplikace Zelle, ale vaše limity převodu budou mnohem nižší.\n\n4. Název uvedený na vašem účtu Haveno MUSÍ odpovídat názvu vašeho účtu Zelle/bankovního účtu.\n\nPokud nemůžete dokončit transakci Zelle, jak je uvedeno ve vaší obchodní smlouvě, můžete ztratit část (nebo vše) ze svého bezpečnostního vkladu.\n\nVzhledem k poněkud vyššímu riziku zpětného zúčtování společnosti Zelle se prodejcům doporučuje kontaktovat nepodepsané kupující prostřednictvím e-mailu nebo SMS, aby ověřili, že kupující skutečně vlastní účet Zelle uvedený v Haveno. -payment.fasterPayments.newRequirements.info=Některé banky začaly ověřovat celé jméno příjemce pro převody Faster Payments. Váš současný účet Faster Payments nepožadoval celé jméno.\n\nZvažte prosím znovu vytvoření svého Faster Payments účtu v Havenou, abyste mohli budoucím kupujícím {0} poskytnout celé jméno.\n\nPři opětovném vytvoření účtu nezapomeňte zkopírovat přesný kód řazení, číslo účtu a hodnoty soli (salt) pro ověření věku ze starého účtu do nového účtu. Tím zajistíte zachování stáří a stavu vašeho stávajícího účtu. -payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci XMR. Potvrzení musí jasně uvádět celé jméno prodejce, zemi, stát a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. -payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. -payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\nUjistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, že používají stejné limity, jaké jsou zde uvedeny.\n\nČástka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\nV případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. +payment.zelle.info=Zelle je služba převodu peněz, která funguje nejlépe *prostřednictvím* jiné banky.\n\n\ + 1. Na této stránce zjistěte, zda (a jak) vaše banka spolupracuje se Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n\ + 2. Zaznamenejte si zvláštní limity převodů - limity odesílání se liší podle banky a banky často určují samostatné denní, týdenní a měsíční limity.\n\n\ + 3. Pokud vaše banka s Zelle nepracuje, můžete ji stále používat prostřednictvím mobilní aplikace Zelle, ale vaše limity převodu budou mnohem nižší.\n\n\ + 4. Název uvedený na vašem účtu Haveno MUSÍ odpovídat názvu vašeho účtu Zelle/bankovního účtu.\n\n\ + Pokud nemůžete dokončit transakci Zelle, jak je uvedeno ve vaší obchodní smlouvě, můžete ztratit část (nebo vše) ze svého bezpečnostního vkladu.\n\n\ + Vzhledem k poněkud vyššímu riziku zpětného zúčtování společnosti Zelle se prodejcům doporučuje kontaktovat nepodepsané kupující \ + prostřednictvím e-mailu nebo SMS, aby ověřili, že kupující skutečně vlastní účet Zelle uvedený v Haveno. +payment.fasterPayments.newRequirements.info=Některé banky začaly ověřovat celé jméno příjemce pro převody \ +Faster Payments. Váš současný účet Faster Payments nepožadoval celé jméno.\n\n\ + Zvažte prosím znovu vytvoření svého Faster Payments účtu v Havenou, abyste mohli budoucím kupujícím {0} poskytnout celé jméno.\n\n\ + Při opětovném vytvoření účtu nezapomeňte zkopírovat přesný kód řazení, číslo účtu a hodnoty soli (salt) pro ověření \ + věku ze starého účtu do nového účtu. Tím zajistíte zachování stáří \ + a stavu vašeho stávajícího účtu. +payment.fasterPayments.ukSortCode="UK sort kód" +payment.moneyGram.info=Při používání MoneyGram musí XMR kupující zaslat autorizační číslo a fotografii potvrzení e-mailem prodejci XMR. \ + Potvrzení musí jasně uvádět celé jméno prodejce, zemi, stát a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. +payment.westernUnion.info=Při používání služby Western Union musí kupující XMR zaslat prodejci XMR e-mailem MTCN (sledovací číslo) a fotografii potvrzení. \ + Potvrzení musí jasně uvádět celé jméno prodejce, město, zemi a částku. E-mail prodávajícího se kupujícímu zobrazí během procesu obchodování. +payment.halCash.info=Při používání HalCash musí kupující XMR poslat prodejci XMR kód HalCash prostřednictvím textové zprávy z mobilního telefonu.\n\n\ + Ujistěte se, že nepřekračujete maximální částku, kterou vám banka umožňuje odesílat pomocí HalCash. \ + Min. částka za výběr je 10 EUR a max. částka je 600 EUR. Pro opakované výběry je to 3000 EUR za příjemce za den \ + a 6000 EUR za příjemce za měsíc. Zkontrolujte prosím tyto limity u své banky, abyste se ujistili, \ + že používají stejné limity, jaké jsou zde uvedeny.\n\n\ + Částka pro výběr musí být násobkem 10 EUR, protože z bankomatu nemůžete vybrat jiné částky. \ + Uživatelské rozhraní na obrazovce vytvořit-nabídku and přijmout-nabídku upraví částku XMR tak, aby částka EUR byla správná. \ + Nemůžete použít tržní cenu, protože částka v EURECH se mění s měnícími se cenami.\n\n\ + V případě sporu musí kupující XMR poskytnout důkaz, že zaslal EURA. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\nU této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\nToto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Uvědomte si, že u všech bankovních převodů existuje určité riziko zpětného zúčtování. Aby se toto riziko zmírnilo, \ + stanoví Haveno limity pro jednotlivé obchody na základě odhadované úrovně rizika zpětného zúčtování pro použitou platební metodu.\n\ + \n\ + U této platební metody je váš limit pro jednotlivé obchody pro nákup a prodej {2}.\n\ + \n\ + Toto omezení se vztahuje pouze na velikost jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ + \n\ + Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n1. Obecné riziko zpětného zúčtování pro platební metodu\n2. Stav podepisování účtu\n\nTento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. Po podpisu se limity nákupu zvýší následovně:\n\n● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\nPodpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\nTato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\nDalší podrobnosti najdete na wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=Aby se omezilo riziko zpětného zúčtování, Haveno stanoví limity pro jednotlivé obchody \ + pro tento typ platebního účtu na základě následujících 2 faktorů:\n\n\ + 1. Obecné riziko zpětného zúčtování pro platební metodu\n\ + 2. Stav podepisování účtu\n\ + \n\ + Tento platební účet ještě není podepsán, takže je omezen na nákup {0} za obchod. \ + Po podpisu se limity nákupu zvýší následovně:\n\ + \n\ + ● Před podpisem a 30 dní po podpisu bude váš limit nákupu podle obchodu {0}\n\ + ● 30 dní po podpisu bude váš limit nákupu podle obchodu {1}\n\ + ● 60 dní po podpisu bude váš limit nákupu podle obchodu {2}\n\ + \n\ + Podpisy účtu neovlivňují prodejní limity. Můžete okamžitě prodat {2} v jednom obchodu.\n\ + \n\ + Tato omezení platí pouze pro objem jednoho obchodu - můžete zadat tolik obchodů, kolik chcete.\n\ + \n\ + Další podrobnosti najdete na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. -payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. Například Bank of America a Wells Fargo již takové vklady nepovolují. +payment.cashDeposit.info=Potvrďte, že vám vaše banka umožňuje odesílat hotovostní vklady na účty jiných lidí. \ + Například Bank of America a Wells Fargo již takové vklady nepovolují. -payment.revolut.info=Revolut vyžaduje „uživatelské jméno“ jako ID účtu, nikoli telefonní číslo nebo e-mail, jako tomu bylo v minulosti. -payment.account.revolut.addUserNameInfo={0}\nVáš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\nChcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\nTo neovlivní stav podepisování věku vašeho účtu. +payment.revolut.info=Revolut vyžaduje 'uživatelské jméno' jako ID účtu, nikoli telefonní číslo nebo e-mail, jako tomu bylo v minulosti. +payment.account.revolut.addUserNameInfo={0}\n\ + Váš stávající účet Revolut ({1}) nemá "Uživatelské jméno".\n\ + Chcete-li aktualizovat údaje o svém účtu, zadejte své "Uživatelské jméno" Revolut.\n\ + To neovlivní stav podepisování stáří vašeho účtu. payment.revolut.addUserNameInfo.headLine=Aktualizujte účet Revolut +payment.cashapp.info=Uvědomte si, že u aplikace Cash App je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. +payment.venmo.info=Mějte na paměti, že u služby Venmo je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. +payment.paypal.info=Uvědomte si prosím, že u služby PayPal je riziko zpětné úhrady (chargeback) vyšší než u většiny bankovních převodů. + payment.amazonGiftCard.upgrade=Platba kartou Amazon eGift nyní vyžaduje také nastavení země. -payment.account.amazonGiftCard.addCountryInfo={0}\nVáš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\nVyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\nTato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. +payment.account.amazonGiftCard.addCountryInfo={0}\n\ + Váš stávající účet pro platbu kartou Amazon eGift ({1}) nemá nastavenou zemi.\n\ + Vyberte prosím zemi, ve které je možné vaše karty Amazon eGift uplatnit.\n\ + Tato aktualizace vašeho účtu nebude mít vliv na stáří tohoto účtu. payment.amazonGiftCard.upgrade.headLine=Aktualizace účtu pro platbu kartou Amazon eGift -payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\n- Kupující XMR musí před odesláním napsat jméno prodejce XMR do polí plátce i příjemce a pořídit fotografii USPMO a obálku s dokladem o sledování ve vysokém rozlišení.\n- Kupující XMR musí odeslat USPMO prodejci XMR s potvrzením dodávky.\n\nV případě, že je nutná mediace, nebo pokud dojde k obchodnímu sporu, budete povinni poslat fotografie mediátorovi Haveno nebo zástupci pro vrácení peněz spolu s pořadovým číslem USPMO, číslem pošty a částkou dolaru, aby mohli ověřit podrobnosti na webu US Post Office.\n\nNeposkytnutí požadovaných informací mediátorovi nebo arbitrovi bude mít za následek ztrátu případu sporu.\n\nVe všech sporných případech nese odesílatel USPMO 100% břemeno odpovědnosti za poskytnutí důkazů mediátorovi nebo arbitrovi.\n\nPokud těmto požadavkům nerozumíte, neobchodujte pomocí USPMO na Haveno. +payment.swift.info.account=Pečlivě si prostudujte základní pokyny pro používání SWIFT v Haveno:\n\ +\n\ +- vyplňte všechna pole kompletně a přesně \n\ +- kupující musí odeslat platbu v měně stanovené tvůrcem nabídky \n\ +- kupující použije u platby model sdílených poplatků (SHA) \n\ +- kupující a prodejce mohou platit poplatky, proto by se měli nejprve seznámit s poplatky své banky \n\ +\n\ +SWIFT je složitější než jiné platební metody, proto si prosím přečtěte kompletní pokyny na wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. + +payment.swift.info.buyer=Pro nákup monero pomocí SWIFT, musíte:\n\ +\n\ +- odeslat platbu v měně, stanovené tvůrcem nabídky \n\ +- použít u platby model sdílených poplatků (SHA) \n\ +\n\ +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. + +payment.swift.info.seller=Odesílatelé SWIFT musí při odesílání plateb používat model sdílených poplatků (SHA).\n\ +\n\ +Pokud obdržíte platbu SWIFT, která nepoužívá SHA, otevřete mediační požadavek.\n\ +\n\ +Přečtěte si další pokyny na wiki, abyste se vyhnuli sankcím a zajistili hladký průběh obchodů [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. + +payment.imps.info.account=Prosím, nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání / přijímání plateb.\n\n\ +Upozorňujeme, že na jednu transakci lze poslat maximálně 200 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí. Uvědomte si však, že jejich maximální limit je 1 000 000 rupií, které lze odeslat za den.\n\n\ +Některé banky mají pro své zákazníky jiné limity. +payment.imps.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Maximální velikost obchodu je 200 000 rupií na transakci.\n\n\ +Pokud váš obchod přesahuje 200 000 rupií, budete muset provést více převodů. Mějte však na paměti, že je stanoven maximální limit 1 000 000 rupií, který lze odeslat za den.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.imps.info.seller=Pokud máte v úmyslu přijmout více než 200 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. Mějte však na paměti, že existuje maximální limit 1 000 000 rupií, které lze odeslat za den.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.neft.info.account=Prosím, nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ +Upozorňujeme, že na jednu transakci lze poslat maximálně 50 000 rupií. Pokud obchodujete s částkou vyšší než tato, bude třeba provést více transakcí.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.neft.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Maximální velikost obchodu je 50 000 rupií na transakci.\n\n\ +Pokud váš obchod přesáhne 50 000 rupií, budete muset provést více převodů.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. +payment.neft.info.seller=Pokud máte v úmyslu získat více než 50 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů.\n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.paytm.info.account=Nezapomeňte uvést e-mail nebo telefonní číslo, které se shoduje s vaším e-mailem nebo telefonním číslem ve vašem účtu PayTM. \n\n\ +Když si uživatelé založí účet PayTM bez KYC, jsou omezeni: \n\n\ + ● Na jednu transakci lze poslat maximálně 5 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 10 000 rupií.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ + ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ +Uživatelé by také měli znát limity účtu. Obchody překračující limity účtu PayTM se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny. +payment.paytm.info.buyer=Platbu zasílejte pouze na uvedenou e-mailovou adresu nebo telefonní číslo.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC.\n\n\ +Bez KYC lze na jednu transakci poslat 5 000 rupií.\n\n\ +S uživateli KYC lze na jednu transakci poslat 100 000 rupií. +payment.paytm.info.seller=Pokud máte v úmyslu obchodovat s částkou vyšší než 5 000 rupií za obchod, budete muset u společnosti PayTM vyplnit KYC. S KYC jsou uživatelé omezeni na:\n\n\ + ● Na jednu transakci lze poslat maximálně 100 000 rupií.\n\ + ● V peněžence PayTM můžete mít maximálně 100 000 rupií.\n\n\ +Uživatelé by také měli znát limity účtu. Protože v peněžence PayTM můžete mít maximálně 100 000 rupií, dbejte na to, abyste své rupie pravidelně převáděli. + +payment.rtgs.info.account=RTGS je určen pro platby velkých obchodů ve výši 200 000 rupií a více.\n\n\ +Při nastavování platebního účtu RTGS nezapomeňte uvést své:\n\n\ + ● Celé jméno majitele účtu\n\ + ● Číslo účtu\n\ + ● IFSC číslo\n\n\\ +Tyto údaje by měly odpovídat vašemu bankovnímu účtu, který budete používat pro odesílání/přijímání plateb.\n\n\ +Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). +payment.rtgs.info.buyer=Platbu prosím zasílejte pouze na účet uvedený v systému Haveno.\n\n\ +Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). +payment.rtgs.info.seller=Vezměte prosím na vědomí, že minimální částka obchodu, kterou lze poslat na jednu transakci, je 200 000 rupií. Pokud obchodujete pod touto částkou, buď bude obchod zrušen, nebo se oba obchodníci budou muset dohodnout na jiném způsobu platby (např. IMPS nebo UPI). + +payment.upi.info.account=Nezapomeňte uvést svou virtuální platební adresu (VPA), která se také nazývá UPI ID. Formát tohoto údaje je podobný e-mailovému ID: se znakem ”@” uprostřed. Vaše UPI ID může být například ”jméno_příjemce@název_banky” nebo ”telefonní_číslo@název_banky”. \n\n\ +Pro UPI je stanoven maximální limit 100 000 rupií, které lze poslat na jednu transakci. \n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 100 000 rupií na jeden obchod, je pravděpodobné, že obchody budou muset proběhnout více převody. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty různé limity. +payment.upi.info.buyer=Platbu prosím zasílejte pouze na VPA / UPI ID uvedené v systému Haveno. \n\n\ +Maximální velikost obchodu je 100 000 rupií na transakci. \n\n\ +Pokud váš obchod přesahuje 100 000 rupií, budete muset provést více převodů. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. +payment.upi.info.seller=Pokud máte v úmyslu přijmout více než 100 000 rupií za obchod, měli byste počítat s tím, že kupující bude muset provést více převodů. \n\n\ +Vezměte prosím na vědomí, že některé banky mají pro své klienty jiné limity. + +payment.celpay.info.account=Nezapomeňte uvést e-mail, na který je váš účet Celsius registrován. \ + Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při jejich přijímání budou připsány na váš účet.\n\n\ +Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ +Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● Stablecoiny USD: DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● Stablecoiny CAD; TrueCAD\n\ + ● Stablecoiny GBP; TrueGBP\n\ + ● Stablecoiny HKD; TrueHKD\n\ + ● Stablecoiny AUD; TrueAUD\n\n\ +Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. +payment.celpay.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu, kterou prodejce XMR uvedl zasláním platebního odkazu.\n\n\ +CelPay je omezena na odeslání 2 500 dolarů (nebo jiné měny/krypto ekvivalentu) za 24 hodin.\n\n\ +Obchody překračující limit účtu CelPay se pravděpodobně budou muset uskutečnit v průběhu více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoiny; TrueCAD\n\ + ● GBP Stablecoiny; TrueGBP\n\ + ● HKD Stablecoiny; TrueHKD\n\ + ● AUD Stablecoiny; TrueAUD\n\n\ +Kupující XMR mohou prodávajícímu XMR poslat libovolnou odpovídající měnu stablecoin. +payment.celpay.info.seller=Prodejci XMR by měli očekávat, že obdrží platbu prostřednictvím zabezpečeného platebního odkazu. \ + Ujistěte se, že odkaz na e-mailovou platbu obsahuje e-mailovou adresu zadanou kupujícím XMR.\n\n\ +Uživatelé CelPay mohou poslat maximálně 2500 dolarů (nebo ekvivalent v jiné měně/krypto) za 24 hodin.\n\n\ +Obchody překračující limity účtu CelPay se pravděpodobně budou muset uskutečnit během více než jednoho dne, nebo budou zrušeny.\n\n\ +CelPay podporuje více stablecoinů:\n\n\ + ● USD Stablecoiny; DAI, TrueUSD, USDC, ZUSD, BUSD, GUSD, PAX, USDT (ERC20)\n\ + ● CAD Stablecoiny; TrueCAD\n\ + ● GBP Stablecoiny; TrueGBP\n\ + ● HKD Stablecoiny; TrueHKD\n\ + ● AUD Stablecoiny; TrueAUD\n\n\ +Prodávající XMR by měli očekávat, že od kupujícího XMR obdrží odpovídající měnu stablecoin. Je možné, aby kupující XMR zaslal libovolnou odpovídající měnu stablecoin. +payment.celpay.supportedCurrenciesForReceiver=Podporované měny (Upozornění: všechny níže uvedené měny jsou v aplikaci Celcius podporovány jako stablecoiny. Obchody se týkají stablecoinů, nikoli fiat.) + +payment.nequi.info.account=Nezapomeňte uvést své telefonní číslo, které je spojeno s vaším účtem Nequi.\n\n\ +Při zakládání účtu Nequi jsou platební limity nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.nequi.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo uvedené v účtu XMR prodávajícího na Havenu.\n\n\ +Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.nequi.info.seller=Zkontrolujte, zda se přijatá platba shoduje s telefonním číslem uvedeným v účtu XMR kupujícího Haveno.\n\n\ +Když si uživatelé založí účet Nequi, jsou limity plateb nastaveny na maximálně ~ 7 000 000 COP, které lze měsíčně odeslat.\n\n\ +Pokud máte v úmyslu obchodovat s částkou vyšší než 7 000 000 COP na jeden obchod, budete muset u společnosti Bancolombia provést KYC a zaplatit poplatek \ + ve výši přibližně 15 000 COP. Poté budou všechny transakce zatíženy 0,4% daní. Ujistěte se prosím, že znáte aktuální výši daní.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.bizum.info.account=K využívání služby Bizum potřebujete bankovní účet (IBAN) ve Španělsku a být zaregistrováni pro tuto službu.\n\n\ +Bizum lze použít pro obchody v rozmezí od 0,50 € do 1 000 €.\n\n\ +Maximální částka transakcí, které můžete odeslat/přijmout prostřednictvím služby Bizum, je 2 000 eur denně.\n\n\ +Uživatelé služby Bizum mohou měsíčně provést maximálně 150 operací.\n\n\ +Každá banka však může pro své klienty stanovit vlastní limity v rámci výše uvedených limitů.\n\n\ +Uživatelé by také měli znát limity účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.bizum.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v systému Haveno.\n\n\ +Maximální velikost obchodu je 1 000 € na jednu platbu. Maximální objem transakcí, které můžete prostřednictvím Bizumu odeslat, je 2 000 eur za den.\n\n\ +Pokud překročíte výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.bizum.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla kupujícího XMR, které je uvedeno v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 EUR na jednu platbu. Maximální objem transakcí, které můžete přijmout pomocí služby Bizum, je 2 000 eur za den.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.pix.info.account=Nezapomeňte uvést vámi vybraný klíč Pix Key. Existují čtyři typy klíčů: \ + CPF (Registr fyzických osob) nebo CNPJ (Národní registr právnických osob), e-mailová adresa, telefonní číslo nebo \ + náhodný klíč vygenerovaný systémem, tzv. univerzální jedinečný identifikátor (UUID). Jiný klíč musí být použit pro \ + každý účet Pix, který máte. Jednotlivci si mohou vytvořit až pět klíčů pro každý účet, který vlastní.\n\n\ +Při obchodování v Haveno by kupující XMR měli používat své klíče Pix v popise platby, aby prodávající XMR mohli snadno identifikovat, že platba pochází od nich. +payment.pix.info.buyer=Platbu prosím zasílejte pouze na Pix Key uvedený v účtu XMR prodávajícího na Haveno.\n\n\ +Jako referenční číslo platby použijte svůj Pix Key, aby prodejce XMR mohl snadno identifikovat, že platba pochází od vás. +payment.pix.info.seller=Zkontrolujte, zda se popis přijaté platby shoduje s klíčem Pix uvedeným v účtu XMR kupujícího na Havenu. +payment.pix.key=Pix Key (CPF, CNPJ, e-mail, telefonní číslo nebo UUID) + +payment.monese.info.account=Monese je bankovní aplikace pro uživatele GBP, EUR a RON*. Monese umožňuje uživatelům posílat peníze do \ + jiných účtů Monese a to okamžitě a zdarma v jakékoli podporované měně.\n\n\ +*Chcete-li si v Monese otevřít účet v RON, musíte buď žít v Rumunsku, nebo mít rumunské občanství.\n\n\ +Při zakládání účtu Monese v Havenu nezapomeňte uvést své jméno a telefonní číslo, které se shoduje s vaším \ + Monese. Tím zajistíte, že se při odesílání peněz zobrazí ze správného účtu a při přijímání \ + budou připsány na váš účet. +payment.monese.info.buyer=Platbu prosím zasílejte pouze na telefonní číslo, které prodejce XMR uvedl ve svém účtu Haveno. Popis platby ponechte nevyplněný. +payment.monese.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu z telefonního čísla / jména uvedeného v účtu XMR kupujícího Haveno. + +payment.satispay.info.account=Pro používání služby Satispay potřebujete bankovní účet (IBAN) v Itálii a být zaregistrován pro tuto službu.\n\n\ +Limity účtu Satispay se nastavují individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. + podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, \ + může být váš obchod zrušen a může být uložena pokuta. +payment.satispay.info.buyer=Platbu prosím zasílejte pouze na mobilní telefonní číslo prodávajícího XMR uvedené v Haveno.\n\n\ +Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, musíte se obrátit na Satispay. + podporu, aby vám zvýšila limity. Uživatelé by si měli být vědomi také limitů na účtu. Pokud obchodujete nad výše uvedené limity \ + může být váš obchod zrušen a může být uložena pokuta. +payment.satispay.info.seller=Ujistěte se prosím, že platba byla přijata z mobilního telefonního čísla / jména kupujícího XMR, jak je uvedeno v Haveno.\n\n\ +Limity účtu Satispay jsou nastaveny individuálně. Pokud chcete obchodovat se zvýšenými částkami, budete muset kontaktovat Satispay \ + podporu, aby vám zvýšila limity. Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity \ + může být váš obchod zrušen a může být uložena pokuta. + +payment.tikkie.info.account=K používání Tikkie potřebujete bankovní účet (IBAN) v Nizozemsku a být zaregistrováni pro tuto službu.\n\n\ +Když pošlete žádost o platbu Tikkie konkrétní osobě, můžete požádat o příjem maximálně 750 EUR na Tikkie \ + žádost. Maximální částka, o kterou můžete požádat během 24 hodin, je 2 500 EUR na jeden účet Tikkie.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtech. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.tikkie.info.buyer=Vyžádejte si prosím platební odkaz od prodejce XMR v chatu obchodníků. Jakmile prodejce XMR \ + zašle platební odkaz, který odpovídá správné částce za obchod, přejděte prosím k platbě.\n\n\ +Když Prodejce XMR požádá o platbu Tikkie, může požádat o platbu maximálně 750 EUR za žádost Tikkie. Pokud \ + je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ + o kterou můžete požádat za den, je 2 500 €.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.tikkie.info.seller=Pošlete prosím odkaz na platbu prodejci XMR pomocí chatu obchodníků. Jakmile vám XMR \ + kupující pošle platbu, zkontrolujte prosím, zda se jeho IBAN shoduje s údaji, které má v Haveno.\n\n\ +Když Prodejce XMR požádá o platbu Tikkie, může požádat maximálně o 750 EUR na jednu žádost Tikkie. Pokud \ + je obchod vyšší než tato částka, bude muset prodejce XMR odeslat více žádostí, aby dosáhl celkové částky obchodu. Maximální částka \ + o které můžete požádat za den, je 2 500 €.\n\n\ +Každá banka však může v rámci těchto limitů stanovit pro své klienty další vlastní limity.\n\n\ +Uživatelé by si také měli být vědomi limitů na účtu. Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.verse.info.account=Verse je způsob platby ve více měnách, který umožňuje odesílat a přijímat platby v EUR, SEK, HUF, DKK, PLN.\n\n\ +Při nastavování účtu Verse v Haveno nezapomeňte uvést uživatelské jméno, které odpovídá vašemu uživatelskému jménu ve \ + Verse. Tím zajistíte, že se při odesílání peněz zobrazí jako odeslané ze správného účtu a při přijímání \ + budou připsány na váš účet.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. +payment.verse.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno, které prodejce XMR uvedl ve svém účtu Haveno. \ + Popis platby prosím ponechte prázdný.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. +payment.verse.info.seller=Prodávající XMR by měli očekávat, že obdrží platbu od uživatelského jména uvedeného v účtu XMR kupujícího na Havenu.\n\n\ +Uživatelé Verse jsou omezeni na odeslání nebo přijetí 10 000 EUR ročně (nebo ekvivalentní částky v cizí měně) pro \ + kumulované platby odeslané z jejich platebního účtu nebo přijaté na jejich platební účet. Tuto částku může Verse na požádání zvýšit. + +payment.achTransfer.info.account=Při přidávání ACH jako platební metody v systému Haveno by se uživatelé měli ujistit, že vědí, \ + kolik peněz bude odeslání a přijetí ACH převodu stát. +payment.achTransfer.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání ACH převodu stát.\n\n\ + Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR pomocí převodu ACH. +payment.achTransfer.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí ACH převodu stát peněz.\n\n\ + Při přijímání platby zkontrolujte, zda je přijata z účtu kupujícího XMR jako převod ACH. + +payment.domesticWire.info.account=Při přidávání tuzemského bankovního převodu jako platební metody v systému Haveno by se uživatelé měli ujistit, že \ + vědí, kolik je bude stát peněz odeslání a přijetí bankovního převodu. +payment.domesticWire.info.buyer=Ujistěte se, že jste si vědomi, kolik vás bude odeslání bankovního převodu stát peněz.\n\n\ + Při platbě zasílejte pouze na platební údaje uvedené v účtu prodávajícího XMR. +payment.domesticWire.info.seller=Ujistěte se prosím, že jste si vědomi toho, kolik vás bude přijetí bankovního převodu stát peněz.\n\n\ + Při přijímání platby zkontrolujte, zda byla přijata z účtu kupujícího XMR. + +payment.strike.info.account=Nezapomeňte uvést své uživatelské jméno Strike.\n\n\ +V systému Haveno se Strike používá pouze pro platby fiat na fiat.\n\n\ +Ujistěte se prosím, že znáte limity Strike:\n\n\ +Uživatelé, kteří se zaregistrovali pouze se svým e-mailem, jménem a telefonním číslem, mají následující limity:\n\n\ + ● maximálně 100 USD na vklad\n\n + ● Maximální celkový vklad 1000 USD za týden\n\n + ● maximálně 100 USD na platbu\n\n\ +Uživatelé mohou své limity zvýšit tím, že společnosti Strike poskytnou více informací. Tito uživatelé mají následující limity:\n\n\n + ● 1 000 USD maximálně na jeden vklad\n\ + ● Maximální celkový objem vkladů za týden ve výši 1 000 USD\n\ + ● 1 000 USD maximálně za platbu\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.strike.info.buyer=Platbu prosím zasílejte pouze na uživatelské jméno Strike prodávajícího XMR, které je uvedené v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. +payment.strike.info.seller=Ujistěte se, že platba byla přijata z uživatelského jména Strike, které patří XMR kupujícímu a které je uvedeno v Haveno.\n\n\ +Maximální velikost obchodu je 1 000 USD na jednu platbu.\n\n\ +Pokud obchodujete nad výše uvedené limity, může být váš obchod zrušen a může vám být uložena pokuta. + +payment.transferwiseUsd.info.account=Vzhledem k bankovní regulaci USA má odesílání a přijímání plateb v USD více omezení \ + než u většiny ostatních měn. Z tohoto důvodu nebyl USD přidán do platební metody Haveno Wise.\n\n\ +Platební metoda Wise-USD umožňuje uživatelům Haveno obchodovat v USD.\n\n\ +Každý, kdo má účet Wise, formálně Wise, může přidat Wise-USD jako platební metodu v systému Haveno. To umožní \ + uživateli nakupovat a prodávat XMR za USD.\n\n\ +Při obchodování na Haveno by kupující XMR neměli uvádět v poznámce žádný důvod platby. Pokud je důvod platby vyžadován, \ + měli by používat pouze celé jméno majitele účtu Wise-USD. +payment.transferwiseUsd.info.buyer=Platbu prosím zasílejte pouze na e-mailovou adresu uvedenou v účtu Haveno Wise-USD prodávajícího XMR. +payment.transferwiseUsd.info.seller=Zkontrolujte, zda se přijatá platba shoduje se jménem kupujícího XMR na účtu Wise-USD v systému Haveno. + +payment.usPostalMoneyOrder.info=Obchodování pomocí amerických poštovních poukázek (USPMO) na Haveno vyžaduje, abyste rozuměli následujícímu:\n\ +\n\ +- Kupující XMR musí před odesláním napsat jméno prodejce XMR do polí plátce i příjemce a pořídit fotografii USPMO a obálku s dokladem o sledování ve vysokém rozlišení.\n\ +- Kupující XMR musí odeslat USPMO prodejci XMR s potvrzením dodávky.\n\ +\n\ +V případě, že je nutná mediace, nebo pokud dojde k obchodnímu sporu, budete povinni poslat fotografie mediátorovi Haveno nebo zástupci pro vrácení peněz spolu s pořadovým číslem USPMO, číslem pošty a částkou dolaru, aby mohli ověřit podrobnosti na webu US Post Office.\n\n\ +Neposkytnutí požadovaných informací mediátorovi nebo arbitrovi bude mít za následek ztrátu případu sporu.\n\n\ +Ve všech sporných případech nese odesílatel USPMO 100% břemeno odpovědnosti za poskytnutí důkazů mediátorovi nebo arbitrovi.\n\n\ +Pokud těmto požadavkům nerozumíte, neobchodujte pomocí USPMO na Haveno. + +payment.payByMail.info=Obchodování pomocí služby Hotovost poštou na Havenu vyžaduje, abyste rozuměli následujícím podmínkám:\n\ + \n\ + ● Kupující XMR by měl zabalit hotovost do sáčku na peníze, který je odolný proti manipulaci.\n\ + ● Kupující XMR by měl natočit nebo vyfotografovat proces balení hotovosti ve vysokém rozlišení s adresou a sledovacím číslem již připevněným na obalu.\n\ + ● Kupující XMR by měl odeslat balíček s hotovostí prodávajícímu XMR s potvrzením o doručení a příslušným pojištěním.\n\ + ● Prodávající XMR by měl natočit otevření balíku a ujistit se, že je na videu vidět sledovací číslo poskytnuté odesílatelem.\n\ + ● Tvůrce nabídky musí uvést veškeré dodatečné podmínky v poli 'Další informace' na platebním účtu.\n\ + ● Příjemce nabídky přijetím nabídky souhlasí s podmínkami tvůrce nabídky.\n\ + \n\ + Obchody Hotovost poštou kladou břemeno jednat čestně rovnoměrně na obě strany.\n\ + \n\ + ● Hotovostní poštovní obchody jsou méně ověřitelné než jiné tradiční obchody. To značně ztěžuje řešení sporů.\n\ + ● Spory se snažte řešit přímo s partnerem pomocí chatu obchodníků. To je nejslibnější cesta k vyřešení jakéhokoli sporu o Hotovost poštou.\n\ + ● Rozhodci mohou váš případ posoudit a přednést své doporučení, ale NEMOHOU vám zaručeně pomoci.\n\ + ● Rozhodci rozhodnou na základě důkazů, které jim budou poskytnuty. Proto dodržujte a dokumentujte výše uvedené postupy, abyste měli důkazy pro případ sporu.\n\ + ● Žádosti o náhradu jakýchkoli ztracených prostředků v důsledku obchodů Hotovost poštou na Haveno NEBUDOU brány v úvahu.\n\ + \n\ + Pokud těmto požadavkům nerozumíte, neobchodujte pomocí Hotovost poštou na Haveno. payment.payByMail.contact=Kontaktní informace payment.payByMail.contact.prompt=Obálka se jménem nebo pseudonymem by měla být adresována +payment.payByMail.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ +Zemi, ve které se nacházíte (např. Francie); \n\ +Země / regiony, ze kterých byste přijímali obchody (např. Francie, EU nebo jakákoli evropská země); \n\ +Jakékoli zvláštní podmínky; \n\ +Jakékoli další údaje. +payment.tradingRestrictions=Přečtěte si prosím podmínky tvůrce.\n\ + Pokud nesplňujete požadavky, nepřijímejte tento obchod. +payment.cashAtAtm.info=Výběr bez karty: Výběr z bankomatu bez karty a pomocí kódu\n\n\ + Použití tohoto způsobu platby:\n\n\ + 1. Vytvořte si platební účet Výběr bez karty a uveďte přijímané banky, regiony nebo jiné podmínky, které se zobrazí u nabídky.\n\n\n\ + 2. Vytvořte nebo přijměte nabídku s tímto platebním účtem.\n\n\ + 3. Po přijetí nabídky se domluvte s obchodním partnerem na čase dokončení platby a sdílejte podrobnosti o platbě.\n\n\ + Pokud se vám nepodaří dokončit transakci, jak je uvedeno v obchodní smlouvě, můžete přijít o část (nebo celou) vaší kauci. +payment.cashAtAtm.extraInfo.prompt=Uveďte prosím ve svých nabídkách: \n\n\ +Vámi přijímané banky / místa; \n\ +Jakékoli zvláštní podmínky; \n\ +Jakékoli další podrobnosti. payment.f2f.contact=Kontaktní informace payment.f2f.contact.prompt=Jak byste chtěli být kontaktováni obchodním partnerem? (e-mailová adresa, telefonní číslo, ...) -payment.f2f.city=Město pro setkání „tváří v tvář“ +payment.f2f.city=Město pro setkání 'tváří v tvář' payment.f2f.city.prompt=Město se zobrazí s nabídkou payment.shared.optionalExtra=Volitelné další informace -payment.shared.extraInfo=Dodatečné informace -payment.shared.extraInfo.prompt=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) -payment.f2f.info=Obchody „tváří v tvář“ mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\nHlavní rozdíly jsou:\n● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n● Obchodní partneři musí přinést své notebooky a na místě setkání potvrdit „platba odeslána“ a „platba přijata“.\n● Pokud má tvůrce speciální „podmínky“, musí uvést podmínky v textovém poli „Další informace“ na účtu.\n● Přijetím nabídky zadavatel souhlasí s uvedenými „podmínkami a podmínkami“ tvůrce.\n● V případě sporu nemůže být mediátor nebo rozhodce příliš nápomocný, protože je obvykle obtížné získat důkazy o tom, co se na schůzce stalo. V takových případech mohou být prostředky XMR uzamčeny na dobu neurčitou nebo dokud se obchodní partneři nedohodnou.\n\nAbyste si byli jisti, že plně rozumíte rozdílům v obchodech „tváří v tvář“, přečtěte si pokyny a doporučení na adrese: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo=Další informace +payment.shared.extraInfo.offer=Další informace o nabídce +payment.shared.extraInfo.prompt.paymentAccount=Uveďte jakékoli speciální požadavky, podmínky a detaily, které chcete zobrazit u vašich nabídek s tímto platebním účtem. (Uživatelé uvidí tyto informace předtím, než akceptují vaši nabídku.) +payment.shared.extraInfo.prompt.offer=Definujte jakékoli speciální podmínky, podmínky nebo detaily, které chcete zobrazit u své nabídky. +payment.shared.extraInfo.noDeposit=Kontaktní údaje a podmínky nabídky +payment.f2f.info=Obchody 'tváří v tvář' mají různá pravidla a přicházejí s jinými riziky než online transakce.\n\n\ + Hlavní rozdíly jsou:\n\ + ● Obchodní partneři si musí vyměňovat informace o místě a čase schůzky pomocí poskytnutých kontaktních údajů.\n\ + ● Obchodní partneři musí přinést své notebooky a na místě setkání potvrdit 'platba odeslána' a 'platba přijata'.\n\ + ● Pokud má tvůrce speciální 'podmínky', musí uvést podmínky v textovém poli 'Další informace' na účtu.\n\ + ● Přijetím nabídky zadavatel souhlasí s uvedenými 'podmínkami a podmínkami' tvůrce.\n\ + ● V případě sporu nemůže být mediátor nebo rozhodce příliš nápomocný, protože je obvykle obtížné získat důkazy o tom, co se na schůzce stalo. \ + V takových případech mohou být prostředky XMR uzamčeny na dobu neurčitou \ + nebo dokud se obchodní partneři nedohodnou.\n\n\ + Abyste si byli jisti, že plně rozumíte rozdílům v obchodech 'tváří v tvář', přečtěte si pokyny a doporučení \ + na adrese: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Otevřít webovou stránku payment.f2f.offerbook.tooltip.countryAndCity=Země a město: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Další informace: {0} +payment.shared.extraInfo.tooltip=Další informace: {0} +payment.ifsc=IFS kód +payment.ifsc.validation=IFSC formát: XXXX0999999 payment.japan.bank=Banka payment.japan.branch=Pobočka @@ -1993,8 +3057,19 @@ payment.japan.account=Účet payment.japan.recipient=Jméno payment.australia.payid=PayID payment.payid=PayID spojené s finanční institucí. Jako e-mailová adresa nebo mobilní telefon. -payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo australské obchodní číslo (ABN), které můžete bezpečně propojit se svou bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\nHaveno zobrazí e-mail nebo mobilní číslo prodejce XMR, kam bude potřeba odeslat tuto dárkovou kartu. Na kartě ve zprávě pro příjemce musí být uvedeno ID obchodu. Pro další detaily a rady viz wiki: [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card].\n\nZde jsou tři důležité poznámky:\n- Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n- Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu. (V takovém případě o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n- Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). +payment.payid.info=PayID jako telefonní číslo, e-mailová adresa nebo australské obchodní číslo (ABN), které můžete bezpečně propojit se svou \ + bankou, družstevní záložnou nebo účtem stavební spořitelny. Musíte mít již vytvořený PayID u své australské finanční instituce. \ + Odesílající i přijímající finanční instituce musí podporovat PayID. Další informace najdete na [HYPERLINK:https://payid.com.au/faqs/] +payment.amazonGiftCard.info=Chcete-li platit dárkovou kartou Amazon eGift, budete muset prodejci XMR poslat kartu Amazon eGift přes svůj účet Amazon.\n\n\ + Podívejte se do wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] pro podrobnosti a rady.\n\n\ + Zde jsou tři důležité poznámky:\n\ + - Preferujte dárkové karty v hodnotě do 100 USD, protože Amazon může považovat nákupy karet s vyššími částkami jako podezřelé a zablokovat je.\n\ + - Na kartě do zprávy pro příjemce můžete přidat i vlastní originální text (např. "Happy birthday Susan!") spolu s ID obchodu (v takovém případě \ + o tom informujte protistranu pomocí obchodovacího chatu, aby mohli s jistotou ověřit, že obdržená dárková karta pochází od vás.)\n\ + - Karty Amazon eGift lze uplatnit pouze na té stránce Amazon, na které byly také koupeny (např. karta koupená na amazon.it může být uplatněna zase jen na amazon.it). +payment.paysafe.info=Pro vaši ochranu důrazně nedoporučujeme používat Paysafecard PINy pro platby.\n\n\ + Transakce provedené pomocí PINů nelze nezávisle ověřit pro řešení sporů. Pokud nastane problém, obnova prostředků nemusí být možná.\n\n\ + Pro zajištění bezpečnosti transakcí a podpory řešení sporů vždy používejte platební metody, které poskytují ověřitelné záznamy. # We use constants from the code so we do not use our normal naming convention @@ -2005,8 +3080,9 @@ NATIONAL_BANK=Národní bankovní převod SAME_BANK=Převod ve stejné bance SPECIFIC_BANKS=Převody u konkrétních bank US_POSTAL_MONEY_ORDER=Poukázka US Postal -CASH_DEPOSIT=Vklad hotovosti na účet prodávajícího -PAY_BY_MAIL=Odeslání hotovosti poštou +CASH_DEPOSIT=Vklad hotovosti +PAY_BY_MAIL=Hotovost poštou +CASH_AT_ATM=Hotovost u bankomatu MONEY_GRAM=MoneyGram WESTERN_UNION=Western Union F2F=Tváří v tvář (osobně) @@ -2026,6 +3102,8 @@ CASH_DEPOSIT_SHORT=Vklad hotovosti # suppress inspection "UnusedProperty" PAY_BY_MAIL_SHORT=Hotovost poštou # suppress inspection "UnusedProperty" +CASH_AT_ATM_SHORT=Hotovost u bankomatu +# suppress inspection "UnusedProperty" MONEY_GRAM_SHORT=MoneyGram # suppress inspection "UnusedProperty" WESTERN_UNION_SHORT=Western Union @@ -2054,7 +3132,7 @@ WECHAT_PAY=WeChat Pay # suppress inspection "UnusedProperty" SEPA=SEPA # suppress inspection "UnusedProperty" -SEPA_INSTANT=SEPA Okamžité platby +SEPA_INSTANT=SEPA okamžité platby # suppress inspection "UnusedProperty" FASTER_PAYMENTS=Faster Payments # suppress inspection "UnusedProperty" @@ -2068,17 +3146,61 @@ INTERAC_E_TRANSFER=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH=HalCash # suppress inspection "UnusedProperty" -BLOCK_CHAINS=Cryptoy +BLOCK_CHAINS=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise +# suppress inspection "UnusedProperty" +TRANSFERWISE_USD=Wise-USD +# suppress inspection "UnusedProperty" +PAYSERA=Paysera +# suppress inspection "UnusedProperty" +PAXUM=Paxum +# suppress inspection "UnusedProperty" +NEFT=India/NEFT +# suppress inspection "UnusedProperty" +RTGS=India/RTGS +# suppress inspection "UnusedProperty" +IMPS=India/IMPS +# suppress inspection "UnusedProperty" +UPI=India/UPI +# suppress inspection "UnusedProperty" +PAYTM=India/PayTM +# suppress inspection "UnusedProperty" +NEQUI=Nequi +# suppress inspection "UnusedProperty" +BIZUM=Bizum +# suppress inspection "UnusedProperty" +PIX=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT=Instantní Cryptoy +BLOCK_CHAINS_INSTANT=Kryptoměny okamžité +# suppress inspection "UnusedProperty" +CAPITUAL=Capitual +# suppress inspection "UnusedProperty" +CELPAY=CelPay +# suppress inspection "UnusedProperty" +MONESE=Monese +# suppress inspection "UnusedProperty" +SATISPAY=Satispay +# suppress inspection "UnusedProperty" +TIKKIE=Tikkie +# suppress inspection "UnusedProperty" +VERSE=Verse +# suppress inspection "UnusedProperty" +STRIKE=Strike +# suppress inspection "UnusedProperty" +SWIFT=SWIFT mezinárodní bankovní převod +# suppress inspection "UnusedProperty" +ACH_TRANSFER=ACH Transfer +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer +# suppress inspection "UnusedProperty" +BSQ_SWAP=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2087,7 +3209,7 @@ OK_PAY=OKPay CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo - +PAYPAL=PayPal # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold @@ -2120,17 +3242,61 @@ INTERAC_E_TRANSFER_SHORT=Interac e-Transfer # suppress inspection "UnusedProperty" HAL_CASH_SHORT=HalCash # suppress inspection "UnusedProperty" -BLOCK_CHAINS_SHORT=Cryptoy +BLOCK_CHAINS_SHORT=Kryptoměny # suppress inspection "UnusedProperty" PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise +# suppress inspection "UnusedProperty" +TRANSFERWISE_USD_SHORT=Wise-USD +# suppress inspection "UnusedProperty" +PAYSERA_SHORT=Paysera +# suppress inspection "UnusedProperty" +PAXUM_SHORT=Paxum +# suppress inspection "UnusedProperty" +NEFT_SHORT=NEFT +# suppress inspection "UnusedProperty" +RTGS_SHORT=RTGS +# suppress inspection "UnusedProperty" +IMPS_SHORT=IMPS +# suppress inspection "UnusedProperty" +UPI_SHORT=UPI +# suppress inspection "UnusedProperty" +PAYTM_SHORT=PayTM +# suppress inspection "UnusedProperty" +NEQUI_SHORT=Nequi +# suppress inspection "UnusedProperty" +BIZUM_SHORT=Bizum +# suppress inspection "UnusedProperty" +PIX_SHORT=Pix # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" -BLOCK_CHAINS_INSTANT_SHORT=Instantní Cryptoy +BLOCK_CHAINS_INSTANT_SHORT=Kryptoměny okamžité +# suppress inspection "UnusedProperty" +CAPITUAL_SHORT=Capitual +# suppress inspection "UnusedProperty" +CELPAY_SHORT=CelPay +# suppress inspection "UnusedProperty" +MONESE_SHORT=Monese +# suppress inspection "UnusedProperty" +SATISPAY_SHORT=Satispay +# suppress inspection "UnusedProperty" +TIKKIE_SHORT=Tikkie +# suppress inspection "UnusedProperty" +VERSE_SHORT=Verse +# suppress inspection "UnusedProperty" +STRIKE_SHORT=Strike +# suppress inspection "UnusedProperty" +SWIFT_SHORT=SWIFT +# suppress inspection "UnusedProperty" +ACH_TRANSFER_SHORT=ACH +# suppress inspection "UnusedProperty" +DOMESTIC_WIRE_TRANSFER_SHORT=Domestic Wire +# suppress inspection "UnusedProperty" +BSQ_SWAP_SHORT=BSQ Swap # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -2139,6 +3305,7 @@ OK_PAY_SHORT=OKPay CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo +PAYPAL_SHORT=PayPal #################################################################### @@ -2193,6 +3360,7 @@ validation.iban.checkSumNotNumeric=Kontrolní součet musí být číselný validation.iban.nonNumericChars=Byl zjištěn nealfanumerický znak validation.iban.checkSumInvalid=Kontrolní součet IBAN je neplatný validation.iban.invalidLength=Číslo musí mít délku 15 až 34 znaků. +validation.iban.sepaNotSupported=SEPA není v této zemi podporována validation.interacETransfer.invalidAreaCode=Non-kanadské směrové číslo oblasti validation.interacETransfer.invalidPhone=Zadejte platné 11místné telefonní číslo (např. 1-123-456-7890) nebo e-mailovou adresu validation.interacETransfer.invalidQuestion=Musí obsahovat pouze písmena, čísla, mezery a/nebo symboly ' _ , . ? - @@ -2215,5 +3383,8 @@ validation.phone.missingCountryCode=K ověření telefonního čísla je potřeb validation.phone.invalidCharacters=Telefonní číslo {0} obsahuje neplatné znaky validation.phone.insufficientDigits=V čísle {0} není dostatek číslic, aby mohlo být platné telefonní číslo validation.phone.tooManyDigits=V čísle {0} je příliš mnoho číslic, než aby mohlo být platné telefonní číslo -validation.phone.invalidDialingCode=Telefonní předvolba země pro číslo {0} je pro zemi {1} neplatná. Správné předčíslí je {2}. +validation.phone.invalidDialingCode=Telefonní předvolba země pro číslo {0} je pro zemi {1} neplatná. \ + Správné předčíslí je {2}. validation.invalidAddressList=Seznam platných adres musí být oddělený čárkami +validation.capitual.invalidFormat=Musí jít o platný kód formátu CAP: CAP-XXXXXX (6 alfanumerických znaků). + diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 3c32e2c38b..4663384a66 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -40,6 +40,8 @@ shared.buyMonero=Monero kaufen shared.sellMonero=Monero verkaufen shared.buyCurrency={0} kaufen shared.sellCurrency={0} verkaufen +shared.buyCurrency.locked={0} kaufen 🔒 +shared.sellCurrency.locked={0} verkaufen 🔒 shared.buyingXMRWith=kaufe XMR mit {0} shared.sellingXMRFor=verkaufe XMR für {0} shared.buyingCurrency=kaufe {0} (verkaufe XMR) @@ -103,7 +105,7 @@ shared.faq=Zur FAQ Seite shared.yesCancel=Ja, abbrechen shared.nextStep=Nächster Schritt shared.selectTradingAccount=Handelskonto auswählen -shared.fundFromSavingsWalletButton=Gelder aus Haveno-Wallet überweisen +shared.fundFromSavingsWalletButton=Wenden Sie Gelder aus der Haveno-Wallet an shared.fundFromExternalWalletButton=Ihre externe Wallet zum Finanzieren öffnen shared.openDefaultWalletFailed=Das Öffnen des Standardprogramms für Monero-Wallets ist fehlgeschlagen. Sind Sie sicher, dass Sie eines installiert haben? shared.belowInPercent=% unter dem Marktpreis @@ -123,6 +125,7 @@ shared.noDateAvailable=Kein Datum verfügbar shared.noDetailsAvailable=Keine Details vorhanden shared.notUsedYet=Noch ungenutzt shared.date=Datum +shared.sendFundsDetailsWithFee=Senden: {0}\n\nAn die Empfangsadresse: {1}\n\nZusätzliche Miner-Gebühr: {2}\n\nSind Sie sicher, dass Sie diesen Betrag senden möchten? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Diese Transaktion würde ein Wechselgeld erzeugen das unterhalb des Dust-Grenzwerts liegt (und daher von den Monero-Konsensregeln nicht erlaubt wäre). Stattdessen wird dieser Dust ({0} Satoshi{1}) der Mining-Gebühr hinzugefügt.\n\n\n shared.copyToClipboard=In Zwischenablage kopieren @@ -139,6 +142,7 @@ shared.addNewAccount=Neues Konto hinzufügen shared.ExportAccounts=Konten exportieren shared.importAccounts=Konten importieren shared.createNewAccount=Neues Konto erstellen +shared.createNewAccountDescription=Ihre Kontodaten werden lokal auf Ihrem Gerät gespeichert und nur mit Ihrem Handelspartner und dem Schiedsrichter geteilt, wenn ein Streitfall eröffnet wird. shared.saveNewAccount=Neues Konto speichern shared.selectedAccount=Konto auswählen shared.deleteAccount=Konto löschen @@ -185,13 +189,14 @@ shared.total=Insgesamt shared.totalsNeeded=Benötigte Gelder shared.tradeWalletAddress=Adresse der Handels-Wallet shared.tradeWalletBalance=Guthaben der Handels-Wallet -shared.reserveExactAmount=Reservieren Sie nur die benötigten Mittel. Es kann erforderlich sein, eine Mining-Gebühr zu zahlen und 10 Bestätigungen (~20 Minuten) abzuwarten, bevor Ihr Angebot aktiv ist. +shared.reserveExactAmount=Reserviere nur die notwendigen Mittel. Erfordert eine Mining-Gebühr und ca. 20 Minuten, bevor dein Angebot live geht. shared.makerTxFee=Ersteller: {0} shared.takerTxFee=Abnehmer: {0} shared.iConfirm=Ich bestätige shared.openURL=Öffne {0} shared.fiat=Fiat shared.crypto=Crypto +shared.preciousMetals=Edelmetalle shared.all=Alle shared.edit=Bearbeiten shared.advancedOptions=Erweiterte Optionen @@ -328,6 +333,7 @@ offerbook.createOffer=Angebot erstellen offerbook.takeOffer=Angebot annehmen offerbook.takeOfferToBuy=Angebot annehmen {0} zu kaufen offerbook.takeOfferToSell=Angebot annehmen {0} zu verkaufen +offerbook.takeOffer.enterChallenge=Geben Sie das Angebots-Passphrase ein offerbook.trader=Händler offerbook.offerersBankId=Bankkennung des Erstellers (BIC/SWIFT): {0} offerbook.offerersBankName=Bankname des Erstellers: {0} @@ -338,6 +344,8 @@ offerbook.availableOffers=Verfügbare Angebote offerbook.filterByCurrency=Nach Währung filtern offerbook.filterByPaymentMethod=Nach Zahlungsmethode filtern offerbook.matchingOffers=Angebote die meinen Zahlungskonten entsprechen +offerbook.filterNoDeposit=Kein Deposit +offerbook.noDepositOffers=Angebote ohne Einzahlung (Passphrase erforderlich) offerbook.timeSinceSigning=Informationen zum Zahlungskonto offerbook.timeSinceSigning.info=Dieses Konto wurde verifiziert und {0} offerbook.timeSinceSigning.info.arbitrator=von einem Vermittler unterzeichnet und kann Partner-Konten unterzeichnen @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=Konto wurde geblockt offerbook.timeSinceSigning.daysSinceSigning={0} Tage offerbook.timeSinceSigning.daysSinceSigning.long={0} seit der Unterzeichnung offerbook.xmrAutoConf=Automatische Bestätigung aktiviert +offerbook.buyXmrWith=XMR kaufen mit: +offerbook.sellXmrFor=XMR verkaufen für: offerbook.timeSinceSigning.help=Wenn Sie einen Trade mit einem Partner erfolgreich abschließen, der ein unterzeichnetes Zahlungskonto hat, wird Ihr Zahlungskonto unterzeichnet.\n{0} Tage später wird das anfängliche Limit von {1} aufgehoben und Ihr Konto kann die Zahlungskonten anderer Partner unterzeichnen. offerbook.timeSinceSigning.notSigned=Noch nicht unterzeichnet @@ -362,6 +372,7 @@ offerbook.nrOffers=Anzahl der Angebote: {0} offerbook.volume={0} (min - max) offerbook.deposit=Kaution XMR (%) offerbook.deposit.help=Kaution die von beiden Handelspartnern bezahlt werden muss, um den Handel abzusichern. Wird zurückgezahlt, wenn der Handel erfolgreich abgeschlossen wurde. +offerbook.createNewOffer=Erstelle Angebot an {0} {1} offerbook.createOfferToBuy=Neues Angebot erstellen, um {0} zu kaufen offerbook.createOfferToSell=Neues Angebot erstellen, um {0} zu verkaufen @@ -453,7 +464,12 @@ createOffer.placeOfferButton=Überprüfung: Anbieten moneros zu {0} createOffer.createOfferFundWalletInfo.headline=Ihr Angebot finanzieren # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Handelsbetrag: {0} \n -createOffer.createOfferFundWalletInfo.msg=Sie müssen zum Annehmen dieses Angebots {0} einzahlen.\n\nDiese Gelder werden in Ihrer lokalen Wallet reserviert und in die MultiSig-Kautionsadresse eingesperrt, wenn jemand Ihr Angebot annimmt.\n\nDer Betrag ist die Summe aus:\n{1}- Kaution: {2}\n- Handelsgebühr: {3}\n- Mining-Gebühr: {4}\n\nSie haben zwei Möglichkeiten, Ihren Handel zu finanzieren:\n- Nutzen Sie Ihre Haveno-Wallet (bequem, aber Transaktionen können nachverfolgbar sein) ODER\n- Von einer externen Wallet überweisen (möglicherweise vertraulicher)\n\nSie werden nach dem Schließen dieses Dialogs alle Finanzierungsmöglichkeiten und Details sehen. +createOffer.createOfferFundWalletInfo.msg=Sie müssen {0} in dieses Angebot einzahlen.\n\n\ + Diese Gelder werden in Ihrer lokalen Wallet reserviert und in eine Multisig-Wallet gesperrt, sobald jemand Ihr Angebot annimmt.\n\n\ + Der Betrag ist die Summe aus:\n\ + {1}\ + - Ihre Sicherheitskaution: {2}\n\ + - Handelsgebühr: {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=Es gab einen Fehler beim Erstellen des Angebots:\n\n{0}\n\nEs haben noch keine Gelder Ihre Wallet verlassen.\nBitte starten Sie Ihre Anwendung neu und überprüfen Sie Ihre Netzwerkverbindung. @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=Meine Kaution als Käufer festlegen (%) createOffer.setDepositForBothTraders=Legen Sie die Kaution für beide Handelspartner fest (%) createOffer.securityDepositInfo=Die Kaution ihres Käufers wird {0} createOffer.securityDepositInfoAsBuyer=Ihre Kaution als Käufer wird {0} -createOffer.minSecurityDepositUsed=Min. Kaution des Käufers wird verwendet +createOffer.minSecurityDepositUsed=Der Mindest-Sicherheitsbetrag wird verwendet. +createOffer.buyerAsTakerWithoutDeposit=Kein Deposit erforderlich vom Käufer (Passphrase geschützt) +createOffer.myDeposit=Meine Sicherheitsleistung (%) +createOffer.myDepositInfo=Ihre Sicherheitsleistung beträgt {0} #################################################################### @@ -499,6 +518,8 @@ takeOffer.fundsBox.networkFee=Gesamte Mining-Gebühr takeOffer.fundsBox.takeOfferSpinnerInfo=Angebot annehmen: {0} takeOffer.fundsBox.paymentLabel=Haveno-Handel mit der ID {0} takeOffer.fundsBox.fundsStructure=({0} Kaution, {1} Handelsgebühr, {2} Mining-Gebühr) +takeOffer.fundsBox.noFundingRequiredTitle=Keine Finanzierung erforderlich +takeOffer.fundsBox.noFundingRequiredDescription=Holen Sie sich das Angebots-Passwort vom Verkäufer außerhalb von Haveno, um dieses Angebot anzunehmen. takeOffer.success.headline=Sie haben erfolgreich ein Angebot angenommen. takeOffer.success.info=Sie können den Status Ihres Trades unter \"Portfolio/Offene Trades\" einsehen. takeOffer.error.message=Bei der Angebotsannahme trat ein Fehler auf.\n\n{0} @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=An einer Service-Abfrage ist ein Fehler a # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Eine Service-Abfrage ist ausgefallen. Eine Automatische Bestätigung ist nicht mehr möglich. -portfolio.pending.step1.info=Die Kautionstransaktion wurde veröffentlicht.\n{0} muss auf wenigstens eine Blockchain-Bestätigung warten, bevor die Zahlung beginnt. +portfolio.pending.step1.info.you=Die Einzahlungstransaktion wurde veröffentlicht.\nSie müssen 10 Bestätigungen abwarten (etwa 20 Minuten), bevor die Zahlung beginnen kann. +portfolio.pending.step1.info.buyer=Die Einzahlungstransaktion wurde veröffentlicht.\nDer XMR-Käufer muss 10 Bestätigungen abwarten (ca. 20 Minuten), bevor die Zahlung gestartet werden kann. portfolio.pending.step1.warn=Die Kautionstransaktion ist noch nicht bestätigt. Dies geschieht manchmal in seltenen Fällen, wenn die Finanzierungsgebühr aus der externen Wallet eines Traders zu niedrig war. portfolio.pending.step1.openForDispute=Die Kautionstransaktion ist noch nicht bestätigt. Sie können länger warten oder den Vermittler um Hilfe bitten. @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=WICHTIGE VORAUSSETZUNG: \nNachd # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Bitte senden Sie {0} per \"US Postal Money Order\" an den XMR-Verkäufer.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Bitte schicken Sie {0} Bargeld per Post an den XMR Verkäufer. Genaue Anweisungen finden Sie im Handelsvertrag, oder Sie stellen über den Handels-Chat Fragen, wenn etwas unklar ist. Weitere Informationen über \"Bargeld per Post\" finden Sie im Haveno-Wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Bitte schicken Sie {0} Bargeld per Post an den XMR Verkäufer. Genaue Anweisungen finden Sie im Handelsvertrag, oder Sie stellen über den Handels-Chat Fragen, wenn etwas unklar ist. Weitere Informationen über \"Bargeld per Post\" finden Sie im Haveno-Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Bitte zahlen Sie {0} mit der gewählten Zahlungsmethode an den XMR Verkäufer. Sie finden die Konto Details des Verkäufers im nächsten Fenster.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -798,11 +820,11 @@ 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/bisq-network/support/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Das Handels-Protokoll hat ein schwerwiegendes Problem gefunden.\n\n{0}\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. portfolio.pending.failedTrade.txChainValid.moveToFailed=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0}\n\nDie Transaktionen des Handels wurden veröffentlicht und die Gelder sind gesperrt. Verschieben Sie den Handel nur dann zu den fehlgeschlagenen Händeln, wenn Sie sich wirklich sicher sind. Dies könnte Optionen zur Behebung des Problems verhindern.\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. @@ -1033,6 +1055,8 @@ settings.net.p2pHeader=Haveno-Netzwerk settings.net.onionAddressLabel=Meine Onion-Adresse settings.net.xmrNodesLabel=Spezifische Monero-Knoten verwenden settings.net.moneroPeersLabel=Verbundene Peers +settings.net.connection=Verbindung +settings.net.connected=Verbunden settings.net.useTorForXmrJLabel=Tor für das Monero-Netzwerk verwenden settings.net.moneroNodesLabel=Mit Monero-Knoten verbinden settings.net.useProvidedNodesRadio=Bereitgestellte Monero-Core-Knoten verwenden @@ -1452,6 +1476,7 @@ offerDetailsWindow.confirm.maker=Bestätigen: Anbieten monero zu {0} offerDetailsWindow.confirm.taker=Bestätigen: Angebot annehmen monero zu {0} offerDetailsWindow.creationDate=Erstellungsdatum offerDetailsWindow.makersOnion=Onion-Adresse des Erstellers +offerDetailsWindow.challenge=Angebots-Passphrase qRCodeWindow.headline=QR Code qRCodeWindow.msg=Bitte nutzen Sie diesen QR Code um Ihr Haveno Wallet von Ihrem externen Wallet aufzuladen. @@ -1492,7 +1517,7 @@ tacWindow.disagree=Ich stimme nicht zu und beende tacWindow.arbitrationSystem=Streitbeilegung tradeDetailsWindow.headline=Handel -tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung: +tradeDetailsWindow.disputedPayoutTxId=Transaktions-ID der strittigen Auszahlung tradeDetailsWindow.tradeDate=Handelsdatum tradeDetailsWindow.txFee=Mining-Gebühr tradeDetailsWindow.tradePeersOnion=Onion-Adresse des Handelspartners @@ -1565,7 +1590,7 @@ popup.headline.error=Fehler popup.doNotShowAgain=Nicht erneut anzeigen popup.reportError.log=Protokolldatei öffnen popup.reportError.gitHub=Auf GitHub-Issue-Tracker melden -popup.reportError={0}\n\nUm uns bei der Verbesserung der Software zu helfen, erstellen Sie bitte einen Fehler-Bericht auf https://github.com/bisq-network/bisq/issues.\nDie Fehlermeldung wird in die Zwischenablage kopiert, wenn Sie auf einen der Knöpfe unten klicken.\nEs wird das Debuggen einfacher machen, wenn Sie die haveno.log Datei anfügen indem Sie "Logdatei öffnen" klicken, eine Kopie speichern und diese dem Fehler-Bericht anfügen. +popup.reportError={0}\n\nUm uns bei der Verbesserung der Software zu helfen, erstellen Sie bitte einen Fehler-Bericht auf https://github.com/haveno-dex/haveno/issues.\nDie Fehlermeldung wird in die Zwischenablage kopiert, wenn Sie auf einen der Knöpfe unten klicken.\nEs wird das Debuggen einfacher machen, wenn Sie die haveno.log Datei anfügen indem Sie "Logdatei öffnen" klicken, eine Kopie speichern und diese dem Fehler-Bericht anfügen. popup.error.tryRestart=Versuchen Sie bitte Ihre Anwendung neu zu starten und überprüfen Sie Ihre Netzwerkverbindung um zu sehen, ob Sie das Problem beheben können. popup.error.takeOfferRequestFailed=Es ist ein Fehler aufgetreten, als jemand versuchte eins Ihrer Angebote anzunehmen:\n{0} @@ -1608,7 +1633,7 @@ popup.warning.nodeBanned=Einer der {0} Nodes wurde gebannt. popup.warning.priceRelay=Preisrelais popup.warning.seed=Seed popup.warning.mandatoryUpdate.trading=Bitte aktualisieren Sie auf die neueste Haveno-Version. Es wurde ein obligatorisches Update veröffentlicht, das den Handel mit alten Versionen deaktiviert. Bitte besuchen Sie das Haveno-Forum für weitere Informationen. -popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed Nodes erhalten. Diese Situation ist unerwartet. Bitte informieren Sie die Haveno Entwickler. +popup.warning.noFilter=Wir haben kein Filterobjekt von den Seed-Knoten erhalten. Bitte informieren Sie die Netzwerkadministratoren, ein Filterobjekt zu registrieren. popup.warning.burnXMR=Die Transaktion ist nicht möglich, da die Mininggebühren von {0} den übertragenen Betrag von {1} überschreiten würden. Bitte warten Sie, bis die Gebühren wieder niedrig sind, oder Sie mehr XMR zum übertragen angesammelt haben. popup.warning.openOffer.makerFeeTxRejected=Die Verkäufergebühren-Transaktion für das Angebot mit der ID {0} wurde vom Monero-Netzwerk abgelehnt.\nTransaktions-ID={1}.\nDas Angebot wurde entfernt, um weitere Probleme zu vermeiden.\nBitte gehen Sie zu \"Einstellungen/Netzwerkinformationen\" und führen Sie eine SPV-Resynchronisierung durch.\nFür weitere Hilfe wenden Sie sich bitte an den Haveno-Support-Kanal des Haveno Keybase Teams. @@ -1678,6 +1703,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys wurden unterzeichnet popup.accountSigning.unsignedPubKeys.result.signed=Unterzeichnete Pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Unterzeichnung fehlgeschlagen +popup.info.buyerAsTakerWithoutDeposit.headline=Kein Depositum vom Käufer erforderlich +popup.info.buyerAsTakerWithoutDeposit=Ihr Angebot erfordert keine Sicherheitsleistung oder Gebühr vom XMR-Käufer.\n\nUm Ihr Angebot anzunehmen, müssen Sie ein Passwort mit Ihrem Handelspartner außerhalb von Haveno teilen.\n\nDas Passwort wird automatisch generiert und nach der Erstellung in den Angebotsdetails angezeigt. + #################################################################### # Notifications #################################################################### @@ -1803,11 +1831,14 @@ navigation.support=\"Support\" formatter.formatVolumeLabel={0} Betrag{1} formatter.makerTaker=Ersteller als {0} {1} / Abnehmer als {2} {3} +formatter.makerTaker.locked=Ersteller als {0} {1} / Abnehmer als {2} {3} 🔒 formatter.youAreAsMaker=Sie sind: {1} {0} (Ersteller) / Abnehmer ist: {3} {2} formatter.youAreAsTaker=Sie sind: {1} {0} (Abnehmer) / Ersteller ist: {3} {2} formatter.youAre=Sie {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Sie erstellen ein Angebot um {1} zu {0} +formatter.youAreCreatingAnOffer.traditional.locked=Sie erstellen ein Angebot um {1} zu {0} 🔒 formatter.youAreCreatingAnOffer.crypto=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) +formatter.youAreCreatingAnOffer.crypto.locked=Sie erstellen ein Angebot {1} zu {0} ({3} zu {2}) 🔒 formatter.asMaker={0} {1} als Ersteller formatter.asTaker={0} {1} als Abnehmer @@ -1950,17 +1981,15 @@ payment.accountType=Kontotyp payment.checking=Überprüfe payment.savings=Ersparnisse payment.personalId=Personalausweis -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle ist ein Geldtransferdienst, der am besten *durch* eine andere Bank funktioniert.\n\n1. Sehen Sie auf dieser Seite nach, ob (und wie) Ihre Bank mit Zelle zusammenarbeitet:\nhttps://www.zellepay.com/get-started\n\n2. Achten Sie besonders auf Ihre Überweisungslimits - die Sendelimits variieren je nach Bank, und die Banken geben oft separate Tages-, Wochen- und Monatslimits an.\n\n3. Wenn Ihre Bank nicht mit Zelle zusammenarbeitet, können Sie die Zahlungsmethode trotzdem über die Zelle Mobile App benutzen, aber Ihre Überweisungslimits werden viel niedriger sein.\n\n4. Der auf Ihrem Haveno-Konto angegebene Name MUSS mit dem Namen auf Ihrem Zelle/Bankkonto übereinstimmen. \n\nWenn Sie eine Zelle Transaktion nicht wie in Ihrem Handelsvertrag angegeben durchführen können, verlieren Sie möglicherweise einen Teil (oder die gesamte) Sicherheitskaution.\n\nWegen des etwas höheren Chargeback-Risikos von Zelle wird Verkäufern empfohlen, nicht unterzeichnete Käufer per E-Mail oder SMS zu kontaktieren, um zu überprüfen, ob der Käufer wirklich das in Haveno angegebene Zelle-Konto besitzt. payment.fasterPayments.newRequirements.info=Einige Banken haben damit begonnen, den vollständigen Namen des Empfängers für Faster Payments Überweisungen zu überprüfen. Ihr aktuelles Faster Payments-Konto gibt keinen vollständigen Namen an.\n\nBitte erwägen Sie, Ihr Faster Payments-Konto in Haveno neu einzurichten, um zukünftigen {0} Käufern einen vollständigen Namen zu geben.\n\nWenn Sie das Konto neu erstellen, stellen Sie sicher, dass Sie die genaue Bankleitzahl, Kontonummer und die "Salt"-Werte für die Altersverifikation von Ihrem alten Konto auf Ihr neues Konto kopieren. Dadurch wird sichergestellt, dass das Alter und der Unterschriftsstatus Ihres bestehenden Kontos erhalten bleiben. payment.moneyGram.info=Bei der Nutzung von MoneyGram, muss der XMR Käufer die MoneyGram Zulassungsnummer und ein Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, das Bundesland des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. payment.westernUnion.info=Bei der Nutzung von Western Union, muss der XMR Käufer die MTCN (Tracking-Nummer) Foto der Quittung per E-Mail an den XMR-Verkäufer senden. Die Quittung muss den vollständigen Namen, das Land, die Stadt des Verkäufers und den Betrag deutlich zeigen. Der Käufer bekommt die E-Mail-Adresse des Verkäufers im Handelsprozess angezeigt. payment.halCash.info=Bei Verwendung von HalCash muss der XMR-Käufer dem XMR-Verkäufer den HalCash-Code per SMS vom Mobiltelefon senden.\n\nBitte achten Sie darauf, dass Sie den maximalen Betrag, den Sie bei Ihrer Bank mit HalCash versenden dürfen, nicht überschreiten. Der Mindestbetrag pro Auszahlung beträgt 10 EUR und der Höchstbetrag 600 EUR. Bei wiederholten Abhebungen sind es 3000 EUR pro Empfänger pro Tag und 6000 EUR pro Empfänger pro Monat. Bitte überprüfen Sie diese Limits bei Ihrer Bank, um sicherzustellen, dass sie die gleichen Limits wie hier angegeben verwenden.\n\nDer Auszahlungsbetrag muss ein Vielfaches von 10 EUR betragen, da Sie keine anderen Beträge an einem Geldautomaten abheben können. Die Benutzeroberfläche beim Erstellen und Annehmen eines Angebots passt den XMR-Betrag so an, dass der EUR-Betrag korrekt ist. Sie können keinen marktbasierten Preis verwenden, da sich der EUR-Betrag bei sich ändernden Preisen ändern würde.\n\nIm Streitfall muss der XMR-Käufer den Nachweis erbringen, dass er die EUR geschickt hat. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Bitte beachten Sie, dass alle Banküberweisungen mit einem gewissen Rückbuchungsrisiko verbunden sind. Um dieses Risiko zu mindern, setzt Haveno Limits pro Trade fest, je nachdem wie hoch das Rückbuchungsrisiko der Zahlungsmethode ist. \n\nFür diese Zahlungsmethode beträgt Ihr Pro-Trade-Limit zum Kaufen oder Verkaufen {2}.\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie Sie möchten.\n\nFinden Sie mehr Informationen im Wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Bitte beachten Sie, dass alle Banküberweisungen mit einem gewissen Rückbuchungsrisiko verbunden sind. Um dieses Risiko zu mindern, setzt Haveno Limits pro Trade fest, je nachdem wie hoch das Rückbuchungsrisiko der Zahlungsmethode ist. \n\nFür diese Zahlungsmethode beträgt Ihr Pro-Trade-Limit zum Kaufen oder Verkaufen {2}.\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie Sie möchten.\n\nFinden Sie mehr Informationen im Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Haveno für diese Zahlungsmethode Limits pro Trade auf der Grundlage der folgenden 2 Faktoren fest:\n\n1. Allgemeines Rückbuchungsrisiko für die Zahlungsmethode\n2. Status der Kontounterzeichnung\n\nDieses Zahlungskonto ist noch nicht unterzeichnet. Es ist daher auf den Kauf von {0} pro Trade beschränkt ist. Nach der Unterzeichnung werden die Kauflimits wie folgt erhöht:\n\n● Vor der Unterzeichnung und für 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {0}\n● 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {1}\n● 60 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {2}\n\nVerkaufslimits sind von der Kontounterzeichnung nicht betroffen. Sie können {2} in einem einzigen Trade sofort verkaufen.\n\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie sie möchten.\n\nWeitere Informationen gibt es im Wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=Um das Risiko einer Rückbuchung zu minimieren, setzt Haveno für diese Zahlungsmethode Limits pro Trade auf der Grundlage der folgenden 2 Faktoren fest:\n\n1. Allgemeines Rückbuchungsrisiko für die Zahlungsmethode\n2. Status der Kontounterzeichnung\n\nDieses Zahlungskonto ist noch nicht unterzeichnet. Es ist daher auf den Kauf von {0} pro Trade beschränkt ist. Nach der Unterzeichnung werden die Kauflimits wie folgt erhöht:\n\n● Vor der Unterzeichnung und für 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {0}\n● 30 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {1}\n● 60 Tage nach der Unterzeichnung beträgt Ihr Kauflimit pro Trade {2}\n\nVerkaufslimits sind von der Kontounterzeichnung nicht betroffen. Sie können {2} in einem einzigen Trade sofort verkaufen.\n\nDieses Limit gilt nur für die Größe eines einzelnen Trades - Sie können soviele Trades platzieren wie sie möchten.\n\nWeitere Informationen gibt es im Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Bitte bestätigen Sie, dass Ihre Bank Bareinzahlungen in Konten von anderen Personen erlaubt. Zum Beispiel werden diese Einzahlungen bei der Bank of America und Wells Fargo nicht mehr erlaubt. @@ -1968,6 +1997,10 @@ payment.revolut.info=Revolut benötigt den "Benutzernamen" als Account ID und ni payment.account.revolut.addUserNameInfo={0}\nDein existierendes Revolut Konto ({1}) hat keinen "Benutzernamen".\nBitte geben Sie Ihren Revolut "Benutzernamen" ein um Ihre Kontodaten zu aktualisieren.\nDas wird Ihr Kontoalter und die Verifizierung nicht beeinflussen. payment.revolut.addUserNameInfo.headLine=Revolut Account updaten +payment.cashapp.info=Bitte beachten Sie, dass Cash App ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. +payment.venmo.info=Bitte beachten Sie, dass Venmo ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. +payment.paypal.info=Bitte beachten Sie, dass PayPal ein höheres Rückbuchungsrisiko hat als die meisten Banküberweisungen. + payment.amazonGiftCard.upgrade=Bei der Zahlungsmethode Amazon Geschenkkarten muss das Land angegeben werden. payment.account.amazonGiftCard.addCountryInfo={0}\nDein bestehendes Amazon Geschenkkarten Konto ({1}) wurde keinem Land zugeteilt.\nBitte geben Sie das Amazon Geschenkkarten Land ein um Ihre Kontodaten zu aktualisieren.\nDas wird ihr Kontoalter nicht beeinflussen. payment.amazonGiftCard.upgrade.headLine=Amazon Geschenkkarte Konto updaten @@ -2001,11 +2034,14 @@ payment.f2f.city=Stadt für ein "Angesicht zu Angesicht" Treffen payment.f2f.city.prompt=Die Stadt wird mit dem Angebot angezeigt payment.shared.optionalExtra=Freiwillige zusätzliche Informationen payment.shared.extraInfo=Zusätzliche Informationen -payment.shared.extraInfo.prompt=Gib spezielle Bedingungen, Abmachungen oder Details die bei ihren Angeboten unter diesem Zahlungskonto angezeigt werden sollen an. Nutzer werden diese Informationen vor der Annahme des Angebots sehen. +payment.shared.extraInfo.offer=Zusätzliche Angebotsinformationen +payment.shared.extraInfo.prompt.paymentAccount=Gib spezielle Bedingungen, Abmachungen oder Details die bei ihren Angeboten unter diesem Zahlungskonto angezeigt werden sollen an. Nutzer werden diese Informationen vor der Annahme des Angebots sehen. +payment.shared.extraInfo.prompt.offer=Definieren Sie alle speziellen Begriffe, Bedingungen oder Details, die Sie mit Ihrem Angebot anzeigen möchten. +payment.shared.extraInfo.noDeposit=Kontaktdaten und Angebotsbedingungen payment.f2f.info=Persönliche 'Face to Face' Trades haben unterschiedliche Regeln und sind mit anderen Risiken verbunden als gewöhnliche Online-Trades.\n\nDie Hauptunterschiede sind:\n● Die Trading Partner müssen die Kontaktdaten und Informationen über den Ort und die Uhrzeit des Treffens austauschen.\n● Die Trading Partner müssen ihre Laptops mitbringen und die Bestätigung der "gesendeten Zahlung" und der "erhaltenen Zahlung" am Treffpunkt vornehmen.\n● Wenn ein Ersteller eines Angebots spezielle "Allgemeine Geschäftsbedingungen" hat, muss er diese im Textfeld "Zusatzinformationen" des Kontos angeben.\n● Mit der Annahme eines Angebots erklärt sich der Käufer mit den vom Anbieter angegebenen "Allgemeinen Geschäftsbedingungen" einverstanden.\n● Im Konfliktfall kann der Mediator oder Arbitrator nicht viel tun, da es in der Regel schwierig ist zu bestimmen, was beim Treffen passiert ist. In solchen Fällen können die Monero auf unbestimmte Zeit oder bis zu einer Einigung der Trading Peers gesperrt werden.\n\nUm sicherzustellen, dass Sie die Besonderheiten der persönlichen 'Face to Face' Trades vollständig verstehen, lesen Sie bitte die Anweisungen und Empfehlungen unter: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webseite öffnen payment.f2f.offerbook.tooltip.countryAndCity=Land und Stadt: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Zusätzliche Informationen: {0} +payment.shared.extraInfo.tooltip=Zusätzliche Informationen: {0} payment.japan.bank=Bank payment.japan.branch=Filiale @@ -2014,7 +2050,10 @@ payment.japan.recipient=Name payment.australia.payid=PayID payment.payid=PayIDs wie E-Mail Adressen oder Telefonnummern die mit Finanzinstitutionen verbunden sind. payment.payid.info=Eine PayID wie eine Telefonnummer, E-Mail Adresse oder Australische Business Number (ABN) mit der Sie sicher Ihre Bank, Kreditgenossenschaft oder Bausparkassenkonto verlinken können. Sie müssen bereits eine PayID mit Ihrer Australischen Finanzinstitution erstellt haben. Beide Institutionen, die die sendet und die die empfängt, müssen PayID unterstützen. Weitere informationen finden Sie unter [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) +payment.amazonGiftCard.info=Um mit einer Amazon eGift Geschenkkarte zu bezahlen, müssen Sie eine Amazon eGift Geschenkkarte über Ihr Amazon-Konto an den XMR-Verkäufer senden. \n\nHaveno zeigt die E-Mail-Adresse oder Telefonnummer des XMR-Verkäufers an, an die die Geschenkkarte gesendet werden soll, und Sie müssen die Handels-ID in das Nachrichtenfeld der Geschenkkarte eintragen. Bitte lesen Sie das Wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] für weitere Details und empfohlene Vorgehensweisen. \n\nDrei wichtige Hinweise:\n- Versuchen Sie Geschenkkarten mit Beträgen von 100 USD oder weniger zu versenden, weil Amazon größere Geschenkkarten gerne als betrügerisch kennzeichnet\n- Versuchen Sie einen kreativen, glaubwürdigen Text für die Nachricht der Geschenkkarten zu verwenden (z.B. "Alles Gute zum Geburtstag Susi!"), zusammen mit der Handels-ID (und verwenden Sie den Handels-Chat, um Ihrem Handelspartner den von Ihnen gewählten Referenztext mitzuteilen, damit er Ihre Zahlung überprüfen kann)\n- Amazon Geschenkkarten können nur auf der Amazon-Website eingelöst werden, auf der sie gekauft wurden (z. B. kann eine auf amazon.it gekaufte Geschenkkarte nur auf amazon.it eingelöst werden) +payment.paysafe.info=Zum Schutz Ihrer Sicherheit raten wir dringend davon ab, Paysafecard-PINs für Zahlungen zu verwenden.\n\n\ + Transaktionen, die über PINs durchgeführt werden, können nicht unabhängig zur Streitbeilegung überprüft werden. Wenn ein Problem auftritt, kann die Rückerstattung von Geldern möglicherweise nicht möglich sein.\n\n\ + Um die Transaktionssicherheit mit Streitbeilegung zu gewährleisten, verwenden Sie immer Zahlungsmethoden, die überprüfbare Aufzeichnungen bieten. # We use constants from the code so we do not use our normal naming convention @@ -2094,7 +2133,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon Gift-Karte # suppress inspection "UnusedProperty" @@ -2146,7 +2185,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon Gift-Karte # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index fb1d4d0b8a..a8f105d6c0 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=Comprando XMR con {0} shared.sellingXMRFor=Vendiendo XMR por {0} shared.buyingCurrency=comprando {0} (Vendiendo XMR) @@ -103,7 +105,7 @@ shared.faq=Visitar web preguntas frecuentes shared.yesCancel=Sí, cancelar shared.nextStep=Siguiente paso shared.selectTradingAccount=Selecionar cuenta de intercambio -shared.fundFromSavingsWalletButton=Transferir fondos desde la cartera Haveno +shared.fundFromSavingsWalletButton=Aplicar fondos desde la billetera de Haveno shared.fundFromExternalWalletButton=Abrir su monedero externo para agregar fondos shared.openDefaultWalletFailed=Fallo al abrir la aplicación de cartera predeterminada. ¿Tal vez no tenga una instalada? shared.belowInPercent=% por debajo del precio de mercado @@ -123,6 +125,7 @@ shared.noDateAvailable=Sin fecha disponible shared.noDetailsAvailable=Sin detalles disponibles shared.notUsedYet=Sin usar aún shared.date=Fecha +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nA la dirección receptora: {1}\n\nTarifa adicional para el minero: {2}\n\n¿Estás seguro de que deseas enviar esta cantidad? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detectó que esta transacción crearía una salida que está por debajo del umbral mínimo considerada polvo (y no está permitida por las reglas de consenso en Monero). En cambio, esta transacción polvo ({0} satoshi {1}) se agregará a la tarifa de minería.\n\n\n shared.copyToClipboard=Copiar al portapapeles @@ -139,6 +142,7 @@ shared.addNewAccount=Añadir una nueva cuenta shared.ExportAccounts=Exportar cuentas shared.importAccounts=Importar cuentas shared.createNewAccount=Crear nueva cuenta +shared.createNewAccountDescription=Los detalles de su cuenta se almacenan localmente en su dispositivo y se comparten solo con su contraparte comercial y el árbitro si se abre una disputa. shared.saveNewAccount=Guardar nueva cuenta shared.selectedAccount=Cuenta seleccionada shared.deleteAccount=Borrar cuenta @@ -185,13 +189,14 @@ shared.total=Total shared.totalsNeeded=Fondos necesarios shared.tradeWalletAddress=Dirección de la cartera para intercambio shared.tradeWalletBalance=Saldo de la cartera de intercambio -shared.reserveExactAmount=Reserve solo los fondos necesarios. Podría requerir una tarifa de minería y 10 confirmaciones (~20 minutos) antes de que su oferta esté activa. +shared.reserveExactAmount=Reserve solo los fondos necesarios. Requiere una tarifa de minería y aproximadamente 20 minutos antes de que tu oferta se haga pública. shared.makerTxFee=Creador: {0} shared.takerTxFee=Tomador: {0} shared.iConfirm=Confirmo shared.openURL=Abrir {0} shared.fiat=Fiat shared.crypto=Cripto +shared.preciousMetals=Metales Preciosos shared.all=Todos shared.edit=Editar shared.advancedOptions=Opciones avanzadas @@ -328,6 +333,7 @@ offerbook.createOffer=Crear oferta offerbook.takeOffer=Tomar oferta offerbook.takeOfferToBuy=Tomar oferta de compra de {0} offerbook.takeOfferToSell=Tomar oferta de venta de {0} +offerbook.takeOffer.enterChallenge=Introduzca la frase secreta de la oferta offerbook.trader=Trader offerbook.offerersBankId=ID del banco del creador (BIC/SWIFT): {0} offerbook.offerersBankName=Nombre del banco del creador: {0} @@ -338,6 +344,8 @@ offerbook.availableOffers=Ofertas disponibles offerbook.filterByCurrency=Filtrar por moneda offerbook.filterByPaymentMethod=Filtrar por método de pago offerbook.matchingOffers=Ofertas que concuerden con mis cuentas +offerbook.filterNoDeposit=Sin depósito +offerbook.noDepositOffers=Ofertas sin depósito (se requiere frase de paso) offerbook.timeSinceSigning=Información de la cuenta offerbook.timeSinceSigning.info=Esta cuenta fue verificada y {0} offerbook.timeSinceSigning.info.arbitrator=firmada por un árbitro y puede firmar cuentas de pares @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=La cuenta fue bloqueada offerbook.timeSinceSigning.daysSinceSigning={0} días offerbook.timeSinceSigning.daysSinceSigning.long={0} desde el firmado offerbook.xmrAutoConf=¿Está habilitada la confirmación automática? +offerbook.buyXmrWith=Compra XMR con: +offerbook.sellXmrFor=Vender XMR por: offerbook.timeSinceSigning.help=Cuando complete con éxito un intercambio con un par que tenga una cuenta de pago firmada, su cuenta de pago es firmada.\n{0} días después, el límite inicial de {1} se eleva y su cuenta puede firmar tras cuentas de pago. offerbook.timeSinceSigning.notSigned=No firmada aún @@ -362,6 +372,7 @@ offerbook.nrOffers=Número de ofertas: {0} offerbook.volume={0} (min - max) offerbook.deposit=Depósito en XMR (%) offerbook.deposit.help=Depósito pagado por cada comerciante para garantizar el intercambio. Será devuelto al acabar el intercambio. +offerbook.createNewOffer=Crear oferta a {0} {1} offerbook.createOfferToBuy=Crear nueva oferta para comprar {0} offerbook.createOfferToSell=Crear nueva oferta para vender {0} @@ -453,7 +464,12 @@ createOffer.placeOfferButton=Revisar: Poner oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Dote de fondos su trato. # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n -createOffer.createOfferFundWalletInfo.msg=Necesita depositar {0} para completar esta oferta.\n\nEsos fondos son reservados en su cartera local y se bloquearán en la dirección de depósito multifirma una vez que alguien tome su oferta.\nLa cantidad es la suma de:\n{1}- Su depósito de seguridad: {2}\n- Comisión de intercambio: {3}\n- Comisión de minado: {4}\n\nPuede elegir entre dos opciones a la hora de depositar fondos para realizar su intercambio:\n- Usar su cartera Haveno (conveniente, pero las transacciones pueden ser trazables) O también\n- Transferir desde una cartera externa (potencialmente con mayor privacidad)\n\nConocerá todos los detalles y opciones para depositar fondos al cerrar esta ventana. +createOffer.createOfferFundWalletInfo.msg=Necesitas depositar {0} para esta oferta.\n\n\ + Estos fondos están reservados en tu billetera local y se bloquearán en una billetera multisig una vez que alguien acepte tu oferta.\n\n\ + El monto es la suma de:\n\ + {1}\ + - Tu depósito de seguridad: {2}\n\ + - Comisión de comercio: {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=Ocurrió un error al colocar la oferta:\n\n{0}\n\nNingún importe de su cartera ha sido deducido aún.\nPor favor, reinicie su aplicación y compruebe su conexión a la red. @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=Establecer mi depósito de seguridad como comprado createOffer.setDepositForBothTraders=Establecer el depósito de seguridad para los comerciantes (%) createOffer.securityDepositInfo=Su depósito de seguridad como comprador será {0} createOffer.securityDepositInfoAsBuyer=Su depósito de seguridad como comprador será {0} -createOffer.minSecurityDepositUsed=En uso el depósito de seguridad mínimo +createOffer.minSecurityDepositUsed=Se utiliza un depósito de seguridad mínimo +createOffer.buyerAsTakerWithoutDeposit=No se requiere depósito del comprador (protegido por passphrase) +createOffer.myDeposit=Mi depósito de seguridad (%) +createOffer.myDepositInfo=Tu depósito de seguridad será {0} #################################################################### @@ -499,6 +518,8 @@ takeOffer.fundsBox.networkFee=Comisiones de minado totales takeOffer.fundsBox.takeOfferSpinnerInfo=Aceptando oferta: {0} takeOffer.fundsBox.paymentLabel=Intercambio Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de seguridad {1} tasa de intercambio, {2} tarifa de minado) +takeOffer.fundsBox.noFundingRequiredTitle=No se requiere financiamiento +takeOffer.fundsBox.noFundingRequiredDescription=Obtén la frase de acceso de la oferta del vendedor fuera de Haveno para aceptar esta oferta. takeOffer.success.headline=Ha aceptado la oferta con éxito. takeOffer.success.info=Puede ver el estado de su intercambio en \"Portafolio/Intercambios abiertos\". takeOffer.error.message=Un error ocurrió al tomar la oferta.\n\n{0} @@ -509,7 +530,7 @@ takeOffer.noPriceFeedAvailable=No puede tomar esta oferta porque utiliza un prec takeOffer.takeOfferFundWalletInfo.headline=Dotar de fondos su intercambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Cantidad a intercambiar: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=Necesita depositar {0} para tomar esta oferta.\n\nLa cantidad es la suma de:\n{1} - Su depósito de seguridad: {2}\n- Comisión de intercambio: {3}\n- Comisiones de minado totales: {4}\n\nPuede elegir entre dos opciones al depositar fondos para realizar su intercambio:\n- Usar su cartera Haveno (conveniente, pero las transacciones pueden ser trazables) O también\n- Transferir desde una cartera externa (potencialmente con mayor privacidad)\n\nVerá todos los detalles y opciones para depositar fondos al cerrar esta ventana. +takeOffer.takeOfferFundWalletInfo.msg=Necesitas depositar {0} para aceptar esta oferta.\n\nLa cantidad es la suma de:\n{1}- Tu depósito de seguridad: {2}\n- Tarifa de transacción: {3} takeOffer.alreadyPaidInFunds=Si ya ha depositado puede retirarlo en la pantalla \"Fondos/Disponible para retirar\". takeOffer.paymentInfo=Información de pago takeOffer.setAmountPrice=Establecer cantidad @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=Ocurrió un error en el servicio solicit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un servicio volvió con algún fallo. No es posible la autoconfirmación. -portfolio.pending.step1.info=La transacción de depósito ha sido publicada.\n{0} tiene que esperar al menos una confirmación en la cadena de bloques antes de comenzar el pago. +portfolio.pending.step1.info.you=La transacción de depósito ha sido publicada.\nNecesitas esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. +portfolio.pending.step1.info.buyer=La transacción de depósito ha sido publicada.\nEl comprador de XMR necesita esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. portfolio.pending.step1.warn=La transacción del depósito aún no se ha confirmado.\nEsto puede suceder en raras ocasiones cuando la tasa de depósito de un comerciante desde una cartera externa es demasiado baja. portfolio.pending.step1.openForDispute=La transacción de depósito aún no ha sido confirmada. Puede esperar más o contactar con el mediador para obtener asistencia. @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUERIMIENTO IMPORTANTE:\nDesp # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envíe {0} mediante \"US Postal Money Order\" a el vendedor de XMR.\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=Por favor envíe {0} usando \"Efectivo por Correo\" al vendedor. Las instrucciones específicas están en el contrato de intercambio, y si no queda claro, pregunte a través del chat de intercambio.\nVea más detalles acerca de Efectivo por Correo en la wiki de Haveno [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail].\n\n +portfolio.pending.step2_buyer.payByMail=Por favor envíe {0} usando \"Efectivo por Correo\" al vendedor. Las instrucciones específicas están en el contrato de intercambio, y si no queda claro, pregunte a través del chat de intercambio.\nVea más detalles acerca de Efectivo por Correo en la wiki de Haveno [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail].\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Por favor pague {0} a través del método de pago especificado al vendedor XMR. Encontrará los detalles de la cuenta del vendedor en la siguiente pantalla.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -798,11 +820,11 @@ 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/bisq-network/support/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. -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/bisq-network/support/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/bisq-network/support/issues]. +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.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]. -portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. +portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.info.popup=El protocolo de intercambio encontró algunos problemas.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=El protocolo de intercambio encontró un problema grave.\n\n{0}\n\n¿Quiere mover la operación a intercambios fallidos?\n\nNo puede abrir mediación o arbitraje desde la vista de operaciones fallidas, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. portfolio.pending.failedTrade.txChainValid.moveToFailed=El protocolo de intercambio encontró algunos problemas.\n\n{0}\n\nLas transacciones del intercambio se han publicado y los fondos están bloqueados. Mueva la operación a operaciones fallidas solo si está realmente seguro. Podría impedir opciones para resolver el problema.\n\n¿Quiere mover la operación a operaciones fallidas?\n\nNo puede abrir mediación o el arbitraje desde la vista de intercambios fallidos, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. @@ -1034,6 +1056,8 @@ settings.net.p2pHeader=Red Haveno settings.net.onionAddressLabel=Mi dirección onion settings.net.xmrNodesLabel=Utilizar nodos Monero personalizados settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexión +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para la red Monero settings.net.moneroNodesLabel=Nodos Monero para conectarse settings.net.useProvidedNodesRadio=Utilizar nodos Monero Core proporcionados @@ -1453,6 +1477,7 @@ offerDetailsWindow.confirm.maker=Confirmar: Poner oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Tomar oferta {0} monero offerDetailsWindow.creationDate=Fecha de creación offerDetailsWindow.makersOnion=Dirección onion del creador +offerDetailsWindow.challenge=Frase de contraseña de la oferta qRCodeWindow.headline=Código QR qRCodeWindow.msg=Por favor, utilice este código QR para fondear su billetera Haveno desde su billetera externa. @@ -1566,7 +1591,7 @@ popup.headline.error=Error popup.doNotShowAgain=No mostrar de nuevo popup.reportError.log=Abrir archivo de registro popup.reportError.gitHub=Reportar al rastreador de problemas de Github -popup.reportError={0}\n\nPara ayudarnos a mejorar el software por favor reporte el fallo en nuestro rastreador de fallos en https://github.com/bisq-network/bisq/issues.\nEl mensaje de error será copiado al portapapeles cuando haga clic en cualquiera de los botones inferiores.\nHará el depurado de fallos más fácil si puede adjuntar el archivo haveno.log presionando "Abrir archivo de log", guardando una copia y adjuntándola en su informe de errores. +popup.reportError={0}\n\nPara ayudarnos a mejorar el software por favor reporte el fallo en nuestro rastreador de fallos en https://github.com/haveno-dex/haveno/issues.\nEl mensaje de error será copiado al portapapeles cuando haga clic en cualquiera de los botones inferiores.\nHará el depurado de fallos más fácil si puede adjuntar el archivo haveno.log presionando "Abrir archivo de log", guardando una copia y adjuntándola en su informe de errores. popup.error.tryRestart=Por favor pruebe reiniciar la aplicación y comprobar su conexión a la red para ver si puede resolver el problema. popup.error.takeOfferRequestFailed=Un error ocurrió cuando alguien intentó tomar una de sus ofertas:\n{0} @@ -1609,7 +1634,7 @@ popup.warning.nodeBanned=Uno de los nodos {0} ha sido baneado. popup.warning.priceRelay=retransmisión de precio popup.warning.seed=semilla popup.warning.mandatoryUpdate.trading=Por favor, actualice a la última versión de Haveno. Se lanzó una actualización obligatoria que inhabilita intercambios con versiones anteriores. Por favor, lea el Foro de Haveno para más información\n -popup.warning.noFilter=No hemos recibido un objeto de filtro desde los nodos semilla. Esta situación no se esperaba. Por favor, informe a los desarrolladores Haveno. +popup.warning.noFilter=No recibimos un objeto de filtro de los nodos semilla. Por favor, informe a los administradores de la red que registren un objeto de filtro. popup.warning.burnXMR=Esta transacción no es posible, ya que las comisiones de minado de {0} excederían la cantidad a transferir de {1}. Por favor, espere a que las comisiones de minado bajen o hasta que haya acumulado más XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=La tasa de transacción para la oferta con ID {0} se rechazó por la red Monero.\nID de transacción={1}\nLa oferta se ha eliminado para evitar futuros problemas.\nPor favor vaya a \"Configuración/Información de red\" y haga una resincronización SPV.\nPara más ayuda por favor contacte con el equipo de soporte de Haveno en el canal de Haveno en Keybase. @@ -1679,6 +1704,9 @@ popup.accountSigning.unsignedPubKeys.signed=Las claves públicas se firmaron popup.accountSigning.unsignedPubKeys.result.signed=Claves públicas firmadas popup.accountSigning.unsignedPubKeys.result.failed=Error al firmar +popup.info.buyerAsTakerWithoutDeposit.headline=No se requiere depósito del comprador +popup.info.buyerAsTakerWithoutDeposit=Tu oferta no requerirá un depósito de seguridad ni una tarifa del comprador de XMR.\n\nPara aceptar tu oferta, debes compartir una frase de acceso con tu compañero de comercio fuera de Haveno.\n\nLa frase de acceso se genera automáticamente y se muestra en los detalles de la oferta después de la creación. + #################################################################### # Notifications #################################################################### @@ -1804,11 +1832,14 @@ navigation.support=\"Soporte\" formatter.formatVolumeLabel={0} cantidad{1} formatter.makerTaker=Creador como {0} {1} / Tomador como {2} {3} +formatter.makerTaker.locked=Creador como {0} {1} / Tomador como {2} {3} 🔒 formatter.youAreAsMaker=Usted es: {1} {0} (creador) / El tomador es: {3} {2} formatter.youAreAsTaker=Usted es: {1} {0} (tomador) / Creador es: {3} {2} formatter.youAre=Usted es {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Está creando una oferta a {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Está creando una oferta a {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Está creando una oferta a {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Está creando una oferta a {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como creador formatter.asTaker={0} {1} como tomador @@ -1951,17 +1982,15 @@ payment.accountType=Tipo de cuenta payment.checking=Comprobando payment.savings=Ahorros payment.personalId=ID personal: -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle es un servicio de transmisión de dinero que funciona mejor *a través* de otro banco..\n\n1. Compruebe esta página para ver si (y cómo) trabaja su banco con Zelle: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. Preste atención a los límites de transferencia -límites de envío- que varían entre bancos, y que los bancos especifican a menudo diferentes límites diarios, semanales y mensuales..\n\n3. Si su banco no trabaja con Zelle, aún puede usarlo a través de la app móvil de Zelle, pero sus límites de transferencia serán mucho menores.\n\n4. El nombre especificado en su cuenta Haveno DEBE ser igual que el nombre en su cuenta de Zelle/bancaria. \n\nSi no puede completar una transacción Zelle tal como se especifica en el contrato, puede perder algo (o todo) el depósito de seguridad!\n\nDebido a que Zelle tiene cierto riesgo de reversión de pago, se aconseja que los vendedores contacten con los compradores no firmados a través de email o SMS para verificar que el comprador realmente tiene la cuenta de Zelle especificada en Haveno. payment.fasterPayments.newRequirements.info=Algunos bancos han comenzado a verificar el nombre completo del receptor para las transferencias Faster Payments. Su cuenta actual Faster Payments no especifica un nombre completo.\n\nConsidere recrear su cuenta Faster Payments en Haveno para proporcionarle a los futuros compradores {0} un nombre completo.\n\nCuando vuelva a crear la cuenta, asegúrese de copiar el UK Short Code de forma precisa , el número de cuenta y los valores salt de la cuenta anterior a su cuenta nueva para la verificación de edad. Esto asegurará que la edad de su cuenta existente y el estado de la firma se conserven. payment.moneyGram.info=Al utilizar MoneyGram, el comprador de XMR tiene que enviar el número de autorización y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el nobre completo del vendedor, país, estado y cantidad. El email del vendedor se mostrará al comprador durante el proceso de intercambio. payment.westernUnion.info=Al utilizar Western Union, el comprador de XMR tiene que enviar el número de seguimiento (MTCN) y una foto del recibo al vendedor de XMR por correo electrónico. El recibo debe mostrar claramente el como el nombre completo del vendedor, país, ciudad y cantidad. Al comprador se le mostrará el correo electrónico del vendedor en el proceso de intercambio. payment.halCash.info=Al usar HalCash el comprador de XMR necesita enviar al vendedor de XMR el código HalCash a través de un mensaje de texto desde el teléfono móvil.\n\nPor favor asegúrese de que no excede la cantidad máxima que su banco le permite enviar con HalCash. La cantidad mínima por retirada es de 10 EUR y el máximo son 600 EUR. Para retiros frecuentes es 3000 por receptor al día y 6000 por receptor al mes. Por favor compruebe estos límites con su banco y asegúrese que son los mismos aquí expuestos.\n\nLa cantidad de retiro debe ser un múltiplo de 10 EUR ya que no se puede retirar otras cantidades desde el cajero automático. La Interfaz de Usuario en la pantalla crear oferta y tomar oferta ajustará la cantidad de XMR para que la cantidad de EUR sea correcta. No puede usar precios basados en el mercado ya que la cantidad de EUR cambiaría con el cambio de precios.\n\nEn caso de disputa el comprador de XMR necesita proveer la prueba de que ha enviado EUR. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Por favor, tenga en cuenta que todas las transferencias bancarias tienen cierto riesgo de reversión de pago.\n\nPara disminuir este riesgo, Haveno establece límites por intercambio en función del nivel estimado de riesgo de reversión de pago para el método usado.\n\nPara este método de pago, su límite por intercambio para comprar y vender es {2}.\n\nEste límite solo aplica al tamaño de un intercambio: puede poner tantos intercambios como quira.\n\nConsulte detalles en la wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Por favor, tenga en cuenta que todas las transferencias bancarias tienen cierto riesgo de reversión de pago.\n\nPara disminuir este riesgo, Haveno establece límites por intercambio en función del nivel estimado de riesgo de reversión de pago para el método usado.\n\nPara este método de pago, su límite por intercambio para comprar y vender es {2}.\n\nEste límite solo aplica al tamaño de un intercambio: puede poner tantos intercambios como quira.\n\nConsulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Haveno establece límites por compra basados en los 2 siguientes factores:\n\n1. Riesgo general de devolución de cargo para el método de pago\n2. Estado de firmado de cuenta\n\nEsta cuenta de pago aún no ha sido firmada, con lo que ha sido limitada para comprar {0} por intercambio. Después de firmarse, los límites de compra se incrementarán de esta manera:\n\n● Antes de ser firmada, y hasta 30 días después de la firma, su límite por intercambio de compra será {0}\n● 30 días después de la firma, su límite de compra por intercambio será de {1}\n● 60 días después de la firma, su límite de compra por intercambio será de {2}\n\nLos límites de venta no se ven afectados por el firmado de cuentas. Puede vender {2} en un solo \nintercambio inmediatamente.\n\nEstos límites solo aplican al tamaño de un intercambio. Puede hacer tantos intercambios como quiera.\n\n Consulte detalles en la wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits].\n\n +payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, Haveno establece límites por compra basados en los 2 siguientes factores:\n\n1. Riesgo general de devolución de cargo para el método de pago\n2. Estado de firmado de cuenta\n\nEsta cuenta de pago aún no ha sido firmada, con lo que ha sido limitada para comprar {0} por intercambio. Después de firmarse, los límites de compra se incrementarán de esta manera:\n\n● Antes de ser firmada, y hasta 30 días después de la firma, su límite por intercambio de compra será {0}\n● 30 días después de la firma, su límite de compra por intercambio será de {1}\n● 60 días después de la firma, su límite de compra por intercambio será de {2}\n\nLos límites de venta no se ven afectados por el firmado de cuentas. Puede vender {2} en un solo \nintercambio inmediatamente.\n\nEstos límites solo aplican al tamaño de un intercambio. Puede hacer tantos intercambios como quiera.\n\n Consulte detalles en la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits].\n\n payment.cashDeposit.info=Por favor confirme que su banco permite enviar depósitos de efectivo a cuentas de otras personas. Por ejemplo, Bank of America y Wells Fargo ya no permiten estos depósitos. @@ -1969,6 +1998,10 @@ payment.revolut.info=Revolut requiere el 'nombre de usuario' como ID de cuenta y payment.account.revolut.addUserNameInfo={0}\nSu cuenta de Revolut ({1}) no tiene un "nombre de usuario".\nPor favor introduzca su "nombre de usuario" en Revolut para actualizar sus datos de cuenta.\nEsto no afectará a su estado de edad de firmado de cuenta. payment.revolut.addUserNameInfo.headLine=Actualizar cuenta Revolut +payment.cashapp.info=Tenga en cuenta que Cash App tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. +payment.venmo.info=Tenga en cuenta que Venmo tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. +payment.paypal.info=Tenga en cuenta que PayPal tiene un mayor riesgo de contracargos que la mayoría de las transferencias bancarias. + payment.amazonGiftCard.upgrade=El método de pago Tarjetas regalo Amazon requiere que se especifique el país payment.account.amazonGiftCard.addCountryInfo={0}\nSu cuenta actual de Tarjeta regalo Amazon ({1}) no tiene un País especificado.\nPor favor introduzca el país de su Tarjeta regalo Amazon para actualizar sus datos de cuenta.\nEsto no afectará el estatus de edad de su cuenta. payment.amazonGiftCard.upgrade.headLine=Actualizar cuenta Tarjeta regalo Amazon @@ -2002,11 +2035,14 @@ payment.f2f.city=Ciudad para la reunión 'cara a cara' payment.f2f.city.prompt=La ciudad se mostrará con la oferta payment.shared.optionalExtra=Información adicional opcional payment.shared.extraInfo=Información adicional -payment.shared.extraInfo.prompt=Defina cualquier término especial, condiciones o detalles que quiera mostrar junto a sus ofertas para esta cuenta de pago (otros usuarios podrán ver esta información antes de aceptar las ofertas). +payment.shared.extraInfo.offer=Información adicional de la oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina cualquier término especial, condiciones o detalles que quiera mostrar junto a sus ofertas para esta cuenta de pago (otros usuarios podrán ver esta información antes de aceptar las ofertas). +payment.shared.extraInfo.prompt.offer=Defina cualquier término, condición o detalle especial que le gustaría mostrar con su oferta. +payment.shared.extraInfo.noDeposit=Detalles de contacto y términos de la oferta payment.f2f.info=Los intercambios 'Cara a Cara' tienen diferentes reglas y riesgos que las transacciones en línea.\n\nLas principales diferencias son:\n● Los pares de intercambio necesitan intercambiar información acerca del punto de reunión y la hora usando los detalles de contacto proporcionados.\n● Los pares de intercambio tienen que traer sus portátiles y hacer la confirmación de 'pago enviado' y 'pago recibido' en el lugar de reunión.\n● Si un creador tiene 'términos y condiciones' especiales necesita declararlos en el campo de texto 'información adicional' en la cuenta.\n● Tomando una oferta el tomador está de acuerdo con los 'términos y condiciones' declarados por el creador.\n● En caso de disputa el árbitro no puede ayudar mucho ya que normalmente es complicado obtener evidencias no manipulables de lo que ha pasado en una reunión. En estos casos los fondos XMR pueden bloquearse indefinidamente o hasta que los pares lleguen a un acuerdo.\n\nPara asegurarse de que comprende las diferencias con los intercambios 'Cara a Cara' por favor lea las instrucciones y recomendaciones en: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Abrir paǵina web payment.f2f.offerbook.tooltip.countryAndCity=País y ciudad: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Información adicional: {0} +payment.shared.extraInfo.tooltip=Información adicional: {0} payment.japan.bank=Banco payment.japan.branch=Branch @@ -2015,7 +2051,10 @@ payment.japan.recipient=Nombre payment.australia.payid=PayID payment.payid=PayID conectado a una institución financiera. Como la dirección email o el número de móvil. payment.payid.info=Un PayID como un número de teléfono, dirección email o Australian Business Number (ABN), que puede conectar con seguridad a su banco, unión de crédito o cuenta de construcción de sociedad. Necesita haber creado una PayID con su institución financiera australiana. Tanto para enviar y recibir las instituciones financieras deben soportar PayID. Para más información por favor compruebe [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) +payment.amazonGiftCard.info=Para pagar con Tarjeta eGift Amazon. necesitará enviar una Tarjeta eGift Amazon al vendedor XMR a través de su cuenta Amazon.\n\nHaveno mostrará la dirección e-mail del vendedor de XMR o el número de teléfono donde la tarjeta de regalo deberá enviarse. Por favor vea la wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] para más detalles y mejores prácticas.\n\nNotas importantes:\n- Pruebe a enviar las tarjetas regalo en cantidades de 100USD o menores, ya que Amazon está señalando tarjetas regalo mayores como fraudulentas.\n- Intente usar textos para el mensaje de la tarjeta regalo creíbles y creativos ("Feliz cumpleaños!").\n- Las tarjetas Amazon eGift pueden ser redimidas únicamente en la web de Amazon en la que se compraron (por ejemplo, una tarjeta comprada en amazon.it solo puede ser redimida en amazon.it) +payment.paysafe.info=Por su protección, desaconsejamos encarecidamente el uso de PINs de Paysafecard para pagos.\n\n\ + Las transacciones realizadas mediante PINs no pueden ser verificadas de forma independiente para la resolución de disputas. Si surge un problema, recuperar los fondos puede no ser posible.\n\n\ + Para garantizar la seguridad de las transacciones con resolución de disputas, utilice siempre métodos de pago que proporcionen registros verificables. # We use constants from the code so we do not use our normal naming convention @@ -2095,7 +2134,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" @@ -2147,7 +2186,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Tarjeta Amazon eGift # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 53a2b2be29..dd0cbee6d3 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -40,6 +40,8 @@ shared.buyMonero=خرید بیتکوین shared.sellMonero=بیتکوین بفروشید shared.buyCurrency=خرید {0} shared.sellCurrency=فروش {0} +shared.buyCurrency.locked=بخر {0} 🔒 +shared.sellCurrency.locked=فروش {0} 🔒 shared.buyingXMRWith=خرید بیتکوین با {0} shared.sellingXMRFor=فروش بیتکوین با {0} shared.buyingCurrency=خرید {0} ( فروش بیتکوین) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=بله، لغو شود shared.nextStep=گام بعدی shared.selectTradingAccount=حساب معاملات را انتخاب کنید -shared.fundFromSavingsWalletButton=انتقال وجه از کیف Haveno +shared.fundFromSavingsWalletButton=اعمال وجه از کیف پول هاونو shared.fundFromExternalWalletButton=برای تهیه پول، کیف پول بیرونی خود را باز کنید shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent= ٪ زیر قیمت بازار @@ -123,6 +125,7 @@ shared.noDateAvailable=تاریخ موجود نیست shared.noDetailsAvailable=جزئیاتی در دسترس نیست shared.notUsedYet=هنوز مورد استفاده قرار نگرفته shared.date=تاریخ +shared.sendFundsDetailsWithFee=ارسال: {0}\n\nبه آدرس گیرنده: {1}\n\nهزینه اضافی ماینر: {2}\n\nآیا مطمئن هستید که می‌خواهید این مبلغ را ارسال کنید؟ # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=کپی در کلیپ‌بورد @@ -139,6 +142,7 @@ shared.addNewAccount=افزودن حساب جدید shared.ExportAccounts=صادر کردن حساب‌ها shared.importAccounts=وارد کردن حساب‌ها shared.createNewAccount=ایجاد حساب جدید +shared.createNewAccountDescription=جزئیات حساب شما به‌طور محلی بر روی دستگاه شما ذخیره شده و تنها با هم‌تجارت شما و داور در صورت باز شدن یک اختلاف به اشتراک گذاشته می‌شود. shared.saveNewAccount=ذخیره‌ی حساب جدید shared.selectedAccount=حساب انتخاب شده shared.deleteAccount=حذف حساب @@ -185,13 +189,14 @@ shared.total=مجموع shared.totalsNeeded=وجه مورد نیاز shared.tradeWalletAddress=آدرس کیف‌پول معاملات shared.tradeWalletBalance=موجودی کیف‌پول معاملات -shared.reserveExactAmount=رزرو فقط مقدار مورد نیاز پول. قبل از فعال شدن پیشنهاد شما، ممکن است نیاز به هزینه استخراج و 10 تایید (~20 دقیقه) باشد. +shared.reserveExactAmount=فقط وجوه مورد نیاز را رزرو کنید. نیاز به هزینه استخراج و حدود ۲۰ دقیقه زمان قبل از فعال شدن پیشنهاد شما دارد. shared.makerTxFee=سفارش گذار: {0} shared.takerTxFee=پذیرنده سفارش: {0} shared.iConfirm=تایید می‌کنم shared.openURL=باز {0} shared.fiat=فیات shared.crypto=کریپتو +shared.preciousMetals=فلزات گرانبها shared.all=همه shared.edit=ویرایش shared.advancedOptions=گزینه‌های پیشرفته @@ -328,6 +333,7 @@ offerbook.createOffer=ایجاد پیشنهاد offerbook.takeOffer=برداشتن پیشنهاد offerbook.takeOfferToBuy=پیشنهاد خرید {0} را بردار offerbook.takeOfferToSell=پیشنهاد فروش {0} را بردار +offerbook.takeOffer.enterChallenge=عبارت عبور پیشنهاد را وارد کنید offerbook.trader=معامله‌گر offerbook.offerersBankId=شناسه بانک سفارش‌گذار (BIC/SWIFT): {0} offerbook.offerersBankName= نام بانک سفارش‌گذار : {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=بانک‌های کشورهای پذیرف offerbook.availableOffers=پیشنهادهای موجود offerbook.filterByCurrency=فیلتر بر اساس ارز offerbook.filterByPaymentMethod=فیلتر بر اساس روش پرداخت -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=پیشنهادات متناسب با حساب‌های من +offerbook.filterNoDeposit=هیچ سپرده‌ای +offerbook.noDepositOffers=پیشنهادهایی بدون ودیعه (نیاز به عبارت عبور) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} روز offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing 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.notSigned=Not signed yet @@ -362,6 +372,7 @@ offerbook.nrOffers=تعداد پیشنهادها: {0} offerbook.volume={0} (حداقل - حداکثر) 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.createNewOffer=پیشنهاد ایجاد کنید به {0} {1} offerbook.createOfferToBuy=پیشنهاد جدید برای خرید {0} ایجاد کن offerbook.createOfferToSell=پیشنهاد جدید برای فروش {0} ایجاد کن @@ -452,7 +463,12 @@ createOffer.placeOfferButton=بررسی: پیشنهاد را برای {0} بی createOffer.createOfferFundWalletInfo.headline=پیشنهاد خود را تامین وجه نمایید # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=مقدار معامله:{0}\n -createOffer.createOfferFundWalletInfo.msg=شما باید {0} برای این پیشنهاد، سپرده بگذارید.\nآن وجوه در کیف پول محلی شما ذخیره شده اند و هنگامی که کسی پیشنهاد شما را دریافت می کند، به آدرس سپرده چند امضایی قفل خواهد شد.\n\nمقدار مذکور، مجموع موارد ذیل است:\n{1} - سپرده‌ی اطمینان شما: {2}\n-هزینه معامله: {3}\n-هزینه تراکنش شبکه: {4}\nشما هنگام تامین مالی معامله‌ی خود، می‌توانید بین دو گزینه انتخاب کنید:\n- از کیف پول Haveno خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. +createOffer.createOfferFundWalletInfo.msg=شما باید {0} را برای این پیشنهاد واریز کنید.\n\n\ +این وجوه در کیف پول محلی شما رزرو می‌شوند و هنگامی که کسی پیشنهاد شما را قبول کند، به یک کیف پول مولتی‌سیگ قفل خواهند شد.\n\n\ +مقدار این مبلغ مجموع موارد زیر است:\n\ +{1}\ +- ودیعه امنیتی شما: {2}\n\ +- هزینه معامله: {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=یک خطا هنگام قرار دادن پیشنهاد، رخ داده است:\n\n{0}\n\nهیچ پولی تاکنون از کیف پول شما کم نشده است.\nلطفاً برنامه را مجدداً راه اندازی کرده و ارتباط اینترنت خود را بررسی نمایید. @@ -474,7 +490,10 @@ createOffer.setDepositAsBuyer=تنظیم سپرده‌ی اطمینان من ب createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=سپرده‌ی اطمینان خریدار شما {0} خواهد بود createOffer.securityDepositInfoAsBuyer=سپرده‌ی اطمینان شما به عنوان خریدار {0} خواهد بود -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=حداقل سپرده امنیتی استفاده می‌شود +createOffer.buyerAsTakerWithoutDeposit=هیچ سپرده‌ای از خریدار مورد نیاز نیست (محافظت شده با پس‌عبارت) +createOffer.myDeposit=سپرده امنیتی من (%) +createOffer.myDepositInfo=ودیعه امنیتی شما {0} خواهد بود #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=کل کارمزد استخراج takeOffer.fundsBox.takeOfferSpinnerInfo=پذیرفتن پیشنهاد: {0} takeOffer.fundsBox.paymentLabel=معامله Haveno با شناسه‌ی {0} takeOffer.fundsBox.fundsStructure=({0} سپرده‌ی اطمینان، {1} هزینه‌ی معامله، {2} هزینه تراکنش شبکه) +takeOffer.fundsBox.noFundingRequiredTitle=نیاز به تأمین مالی نیست +takeOffer.fundsBox.noFundingRequiredDescription=برای پذیرش این پیشنهاد، رمزعبور آن را از فروشنده خارج از هاونئو دریافت کنید. takeOffer.success.headline=با موفقیت یک پیشنهاد را قبول کرده‌اید. takeOffer.success.info=شما می‌توانید وضعیت معامله‌ی خود را در \"سبد سهام /معاملات باز\" ببینید. takeOffer.error.message=هنگام قبول کردن پیشنهاد، اتفاقی رخ داده است.\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=امکان پذیرفتن پیشنهاد وجود takeOffer.takeOfferFundWalletInfo.headline=معامله خود را تأمین وجه نمایید # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=مقدار معامله: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=شما باید {0} برای قبول این پیشنهاد، سپرده بگذارید.\nاین مقدار مجموع موارد ذیل است:\n{1} - سپرده‌ی اطمینان شما: {2}\n-هزینه معامله: {3}\n-تمامی هزینه های تراکنش شبکه: {4}\nشما هنگام تامین مالی معامله‌ی خود، می‌توانید بین دو گزینه انتخاب کنید:\n- از کیف پول Haveno خود استفاده کنید (این روش راحت است، اما ممکن است تراکنش‌ها قابل رصد شوند)، یا\n- از کیف پول خارجی انتقال دهید (به طور بالقوه‌ای این روش ایمن‌تر و محافظ حریم خصوصی شما است)\n\nشما تمام گزینه‌ها و جزئیات تامین مالی را پس از بستن این پنجره، خواهید دید. +takeOffer.takeOfferFundWalletInfo.msg=باید {0} را برای پذیرش این پیشنهاد واریز کنید.\n\nمبلغ مجموع موارد زیر است:\n{1}- سپرده امنیتی شما: {2}\n- هزینه معامله: {3} takeOffer.alreadyPaidInFunds=اگر شما در حال حاضر در وجوه، پرداختی داشته اید، می توانید آن را در صفحه ی \"وجوه/ارسال وجوه\" برداشت کنید. takeOffer.paymentInfo=اطلاعات پرداخت takeOffer.setAmountPrice=تنظیم مقدار @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=تراکنش سپرده منتشر شده است.\nباید برای حداقل یک تأییدیه بلاک چین قبل از آغاز پرداخت، {0} صبر کنید. +portfolio.pending.step1.info.you=تراکنش واریز منتشر شده است.\nشما باید منتظر 10 تاییدیه (حدود 20 دقیقه) باشید تا پرداخت آغاز شود. +portfolio.pending.step1.info.buyer=تراکنش واریز منتشر شده است.\nخریدار XMR باید منتظر ۱۰ تاییدیه (حدود ۲۰ دقیقه) باشد تا پرداخت آغاز شود. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=مورد الزامی مهم:\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=لطفاً {0} را توسط \"US Postal Money Order\" به فروشنده‌ی بیتکوین پرداخت کنید.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1030,6 +1052,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=آدرس onion من settings.net.xmrNodesLabel=استفاده از گره‌های Monero اختصاصی settings.net.moneroPeersLabel=همتایان متصل +settings.net.connection=اتصال +settings.net.connected=متصل settings.net.useTorForXmrJLabel=استفاده از Tor برای شبکه مونرو settings.net.moneroNodesLabel=گره‌های Monero در دسترس settings.net.useProvidedNodesRadio=استفاده از نودهای بیتکوین ارائه شده @@ -1448,6 +1472,7 @@ offerDetailsWindow.confirm.maker=تأیید: پیشنهاد را به {0} بگذ offerDetailsWindow.confirm.taker=تأیید: پیشنهاد را به {0} بپذیرید offerDetailsWindow.creationDate=تاریخ ایجاد offerDetailsWindow.makersOnion=آدرس Onion سفارش گذار +offerDetailsWindow.challenge=Passphrase de l'offre qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1561,7 +1586,7 @@ popup.headline.error=خطا popup.doNotShowAgain=دوباره نشان نده popup.reportError.log=باز کردن فایل گزارش popup.reportError.gitHub=گزارش به پیگیر مسائل GitHub  -popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/bisq-network/bisq/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. +popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=لطفاً سعی کنید برنامه را مجدداً راه اندازی کنید و اتصال شبکه خود را بررسی کنید تا ببینید آیا می توانید مشکل را حل کنید یا خیر. popup.error.takeOfferRequestFailed=وقتی کسی تلاش کرد تا یکی از پیشنهادات شما را بپذیرد خطایی رخ داد:\n{0} @@ -1604,6 +1629,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. 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.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. @@ -1671,6 +1697,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=هیچ پیش‌پرداختی از خریدار مورد نیاز نیست +popup.info.buyerAsTakerWithoutDeposit=پیشنهاد شما نیاز به ودیعه امنیتی یا هزینه از خریدار XMR ندارد.\n\nبرای پذیرفتن پیشنهاد شما، باید یک پس‌عبارت را با شریک تجاری خود خارج از Haveno به اشتراک بگذارید.\n\nپس‌عبارت به‌طور خودکار تولید می‌شود و پس از ایجاد در جزئیات پیشنهاد نمایش داده می‌شود. + #################################################################### # Notifications #################################################################### @@ -1796,11 +1825,14 @@ navigation.support=\"پشتیبانی\" formatter.formatVolumeLabel={0} مبلغ {1} formatter.makerTaker=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} +formatter.makerTaker.locked=سفارش گذار به عنوان {0} {1} / پذیرنده به عنوان {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=شما {0} {1} ({2} {3}) هستید formatter.youAreCreatingAnOffer.traditional=شما در حال ایجاد یک پیشنهاد به {0} {1} هستید +formatter.youAreCreatingAnOffer.traditional.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} هستید formatter.youAreCreatingAnOffer.crypto=شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید +formatter.youAreCreatingAnOffer.crypto.locked=🔒 شما در حال ایجاد یک پیشنهاد به {0} {1} ({2} {3}) هستید formatter.asMaker={0} {1} به عنوان سفارش گذار formatter.asTaker={0} {1} به عنوان پذیرنده @@ -1943,17 +1975,15 @@ payment.accountType=نوع حساب payment.checking=بررسی payment.savings=اندوخته ها payment.personalId=شناسه شخصی -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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 باید شواهد مربوط به ارسال یورو را ارائه دهد. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. @@ -1961,12 +1991,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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 ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. +payment.venmo.info=لطفاً توجه داشته باشید که Venmo ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. +payment.paypal.info=لطفاً توجه داشته باشید که PayPal ریسک بازپرداخت بالاتری نسبت به بیشتر انتقالات بانکی دارد. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=اطلاعات تماس payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=اطلاعات تماس @@ -1975,11 +2007,13 @@ payment.f2f.city=شهر جهت ملاقات 'رو در رو' payment.f2f.city.prompt=نام شهر به همراه پیشنهاد نمایش داده خواهد شد payment.shared.optionalExtra=اطلاعات اضافی اختیاری payment.shared.extraInfo=اطلاعات اضافی -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=اطلاعات اضافی پیشنهاد +payment.shared.extraInfo.prompt.paymentAccount=هرگونه اصطلاحات، شرایط یا جزئیات خاصی که می‌خواهید همراه با پیشنهادات شما برای این حساب پرداخت نمایش داده شود را تعریف کنید (کاربران قبل از پذیرش پیشنهادات این اطلاعات را مشاهده خواهند کرد). +payment.shared.extraInfo.prompt.offer=هر اصطلاح، شرایط یا جزئیات خاصی که مایلید همراه با پیشنهاد خود نمایش داده شود را تعریف کنید. +payment.shared.extraInfo.noDeposit=جزئیات تماس و شرایط پیشنهاد payment.f2f.info.openURL=باز کردن صفحه وب payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=اطلاعات اضافی: {0} +payment.shared.extraInfo.tooltip=اطلاعات اضافی: {0} payment.japan.bank=بانک payment.japan.branch=Branch @@ -1988,7 +2022,10 @@ 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://haveno.exchange/wiki/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\ + برای اطمینان از امنیت تراکنش و حل اختلاف، همیشه از روش‌های پرداختی استفاده کنید که سوابق قابل تاییدی ارائه می‌دهند. # We use constants from the code so we do not use our normal naming convention @@ -2068,7 +2105,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2120,7 +2157,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index ec42105c9c..f4395ca9c4 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -40,6 +40,8 @@ shared.buyMonero=Achat Monero shared.sellMonero=Vendre des Moneros shared.buyCurrency=Achat {0} shared.sellCurrency=Vendre {0} +shared.buyCurrency.locked=Achat {0} 🔒 +shared.sellCurrency.locked=Vendre {0} 🔒 shared.buyingXMRWith=achat XMR avec {0} shared.sellingXMRFor=vendre XMR pour {0} shared.buyingCurrency=achat {0} (vente XMR) @@ -103,7 +105,7 @@ shared.faq=Visitez la page FAQ shared.yesCancel=Oui, annuler shared.nextStep=Étape suivante shared.selectTradingAccount=Sélectionner le compte de trading -shared.fundFromSavingsWalletButton=Transférer des fonds depuis le portefeuille Haveno +shared.fundFromSavingsWalletButton=Appliquer les fonds depuis le portefeuille Haveno shared.fundFromExternalWalletButton=Ouvrez votre portefeuille externe pour provisionner shared.openDefaultWalletFailed=L'ouverture de l'application de portefeuille Monero par défaut a échoué. Êtes-vous sûr de l'avoir installée? shared.belowInPercent=% sous le prix du marché @@ -123,6 +125,7 @@ shared.noDateAvailable=Pas de date disponible shared.noDetailsAvailable=Pas de détails disponibles shared.notUsedYet=Pas encore utilisé shared.date=Date +shared.sendFundsDetailsWithFee=Envoyer : {0}\n\nÀ l'adresse de réception : {1}\n\nFrais supplémentaires pour le mineur : {2}\n\nÊtes-vous sûr de vouloir envoyer ce montant ? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno détecte que la transaction produira une sortie inférieure au seuil de fraction minimum (non autorisé par les règles de consensus Monero). Au lieu de cela, ces fractions ({0} satoshi {1}) seront ajoutées aux frais de traitement minier.\n\n\n shared.copyToClipboard=Copier dans le presse-papiers @@ -139,6 +142,7 @@ shared.addNewAccount=Ajouter un nouveau compte shared.ExportAccounts=Exporter les comptes shared.importAccounts=Importer les comptes shared.createNewAccount=Créer un nouveau compte +shared.createNewAccountDescription=Les détails de votre compte sont stockés localement sur votre appareil et partagés uniquement avec votre pair de trading et l'arbitre si un litige est ouvert. shared.saveNewAccount=Sauvegarder un nouveau compte shared.selectedAccount=Sélectionner un compte shared.deleteAccount=Supprimer le compte @@ -185,13 +189,14 @@ shared.total=Total shared.totalsNeeded=Fonds nécessaires shared.tradeWalletAddress=Adresse du portefeuille de trading shared.tradeWalletBalance=Solde du portefeuille de trading -shared.reserveExactAmount=Réservez uniquement les fonds nécessaires. Il peut être nécessaire de payer des frais de minage et d'attendre 10 confirmations (~20 minutes) avant que votre offre ne soit active. +shared.reserveExactAmount=Réservez uniquement les fonds nécessaires. Nécessite des frais de minage et environ 20 minutes avant que votre offre ne soit mise en ligne. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Je confirme shared.openURL=Ouvert {0} shared.fiat=Fiat shared.crypto=Crypto +shared.preciousMetals=Métaux précieux shared.all=Tout shared.edit=Modifier shared.advancedOptions=Options avancées @@ -328,6 +333,7 @@ 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.takeOffer.enterChallenge=Entrez la phrase secrète de l'offre offerbook.trader=Échanger offerbook.offerersBankId=ID de la banque du maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nom de la banque du maker: {0} @@ -338,6 +344,8 @@ offerbook.availableOffers=Ordres disponibles offerbook.filterByCurrency=Filtrer par devise offerbook.filterByPaymentMethod=Filtrer par mode de paiement offerbook.matchingOffers=Offres correspondants à mes comptes +offerbook.filterNoDeposit=Aucun dépôt +offerbook.noDepositOffers=Offres sans dépôt (passphrase requise) offerbook.timeSinceSigning=Informations du compte offerbook.timeSinceSigning.info=Ce compte a été vérifié et {0} offerbook.timeSinceSigning.info.arbitrator=signé par un arbitre et pouvant signer des comptes pairs @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=Ce compte a été banni offerbook.timeSinceSigning.daysSinceSigning={0} jours offerbook.timeSinceSigning.daysSinceSigning.long={0} depuis la signature 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.notSigned=Pas encore signé @@ -362,6 +372,7 @@ 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.createOfferToSell=Créer un nouvel ordre de vente pour {0} @@ -453,7 +464,12 @@ createOffer.placeOfferButton=Review: Placer un ordre de {0} monero createOffer.createOfferFundWalletInfo.headline=Financer votre ordre # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=Montant du trade: {0}\n\n -createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} pour cet ordre.\n\nCes fonds sont réservés dans votre portefeuille local et seront bloqués sur une adresse de dépôt multisig une fois que quelqu''un aura accepté votre ordre.\n\nLe montant correspond à la somme de:\n{1}- Votre dépôt de garantie: {2}\n- Frais de trading: {3}\n- Frais d''exploitation minière: {4}\n\nVous avez le choix entre deux options pour financer votre transaction :\n- Utilisez votre portefeuille Haveno (pratique, mais les transactions peuvent être associables) OU\n- Transfert depuis un portefeuille externe (potentiellement plus privé)\n\nVous pourrez voir toutes les options de financement et les détails après avoir fermé ce popup. +createOffer.createOfferFundWalletInfo.msg=Vous devez déposer {0} à cette offre.\n\n\ + Ces fonds sont réservés dans votre portefeuille local et seront verrouillés dans un portefeuille multisignature dès qu'une personne acceptera votre offre.\n\n\ + Le montant est la somme de :\n\ + {1}\ + - Votre dépôt de garantie : {2}\n\ + - 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. @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=Définir mon dépôt de garantie en tant qu'achete 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.minSecurityDepositUsed=Le minimum de dépôt de garantie de l'acheteur est utilisé +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 (%) +createOffer.myDepositInfo=Votre dépôt de garantie sera de {0} #################################################################### @@ -499,6 +518,8 @@ 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.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} @@ -509,7 +530,7 @@ takeOffer.noPriceFeedAvailable=Vous ne pouvez pas accepter cet ordre, car celui- takeOffer.takeOfferFundWalletInfo.headline=Provisionner votre trade # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Montant du trade: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=Vous devez envoyer {0} pour cet odre.\n\nLe montant est la somme de:\n{1}--Dépôt de garantie: {2}\n- Frais de transaction: {3}\n- Frais de minage: {4}\n\nVous avez deux choix pour payer votre transaction :\n- Utiliser votre portefeuille local Haveno (pratique, mais vos transactions peuvent être tracées) OU\n- Transférer d''un portefeuille externe (potentiellement plus confidentiel)\n\nVous retrouverez toutes les options de provisionnement après fermeture de ce popup. +takeOffer.takeOfferFundWalletInfo.msg=Vous devez déposer {0} pour accepter cette offre.\n\nLe montant est la somme de :\n{1}- Votre dépôt de garantie : {2}\n- Frais de transaction : {3} takeOffer.alreadyPaidInFunds=Si vous avez déjà provisionner des fonds vous pouvez les retirer dans l'onglet \"Fonds/Envoyer des fonds\". takeOffer.paymentInfo=Informations de paiement takeOffer.setAmountPrice=Définir le montant @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=Une erreur lors de la demande du service # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un service a retourné un échec. L'auto-confirmation n'est pas possible. -portfolio.pending.step1.info=La transaction de dépôt à été publiée.\n{0} devez attendre au moins une confirmation de la blockchain avant d''initier le paiement. +portfolio.pending.step1.info.you=La transaction de dépôt a été publiée.\nVous devez attendre 10 confirmations (environ 20 minutes) avant que le paiement ne puisse commencer. +portfolio.pending.step1.info.buyer=La transaction de dépôt a été publiée.\nL'acheteur XMR doit attendre 10 confirmations (environ 20 minutes) avant que le paiement puisse commencer. portfolio.pending.step1.warn=La transaction de dépôt n'est toujours pas confirmée. Cela se produit parfois dans de rares occasions lorsque les frais de financement d'un trader en provenance d'un portefeuille externe sont trop bas. portfolio.pending.step1.openForDispute=La transaction de dépôt n'est toujours pas confirmée. Vous pouvez attendre plus longtemps ou contacter le médiateur pour obtenir de l'aide. @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=CONDITIONS REQUISES:\nAprès av # suppress inspection "TrailingSpacesInProperty" 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://haveno.exchange/wiki/Cash_by_Mail]\n +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" @@ -799,11 +821,11 @@ 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/bisq-network/support/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Le protocole de trade a rencontré quelques problèmes/\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Le protocole de trade a rencontré un problème critique.\n\n{0}\n\nVoulez-vous déplacer ce trade vers les trades échoués?\n\nVous ne pouvez pas ouvrir de médiations ou de jugements depuis la liste des trades échoués, mais vous pouvez redéplacer un trade échoué vers l'écran des trades ouverts quand vous le souhaitez. portfolio.pending.failedTrade.txChainValid.moveToFailed=Il y a des problèmes avec cet accord de transaction. \n\n{0}\n\nLa transaction de devis a été validée et les fonds ont été bloqués. Déplacer la transaction vers une transaction échouée uniquement si elle est certaine. Cela peut empêcher les options disponibles pour résoudre le problème. \n\nÊtes-vous sûr de vouloir déplacer cette transaction vers la transaction échouée? \n\nVous ne pouvez pas ouvrir une médiation ou un arbitrage dans une transaction échouée, mais vous pouvez déplacer une transaction échouée vers la transaction incomplète à tout moment. @@ -1035,6 +1057,8 @@ settings.net.p2pHeader=Le réseau Haveno settings.net.onionAddressLabel=Mon adresse onion settings.net.xmrNodesLabel=Utiliser des nœuds Monero personnalisés settings.net.moneroPeersLabel=Pairs connectés +settings.net.connection=Connexion +settings.net.connected=Connecté settings.net.useTorForXmrJLabel=Utiliser Tor pour le réseau Monero settings.net.moneroNodesLabel=Nœuds Monero pour se connecter à settings.net.useProvidedNodesRadio=Utiliser les nœuds Monero Core fournis @@ -1454,6 +1478,7 @@ offerDetailsWindow.confirm.maker=Confirmer: Placer un ordre de {0} monero offerDetailsWindow.confirm.taker=Confirmer: Acceptez l''ordre de {0} monero offerDetailsWindow.creationDate=Date de création offerDetailsWindow.makersOnion=Adresse onion du maker +offerDetailsWindow.challenge=Phrase secrète de l'offre qRCodeWindow.headline=QR Code qRCodeWindow.msg=Veuillez utiliser le code QR pour recharger du portefeuille externe au portefeuille Haveno. @@ -1494,7 +1519,7 @@ tacWindow.disagree=Je ne suis pas d'accord et je quitte tacWindow.arbitrationSystem=Règlement du litige tradeDetailsWindow.headline=Échange -tradeDetailsWindow.disputedPayoutTxId=ID de la transaction de versement contestée : +tradeDetailsWindow.disputedPayoutTxId=ID de la transaction de versement contestée tradeDetailsWindow.tradeDate=Date de l'échange tradeDetailsWindow.txFee=Frais de minage tradeDetailsWindow.tradePeersOnion=Adresse onion du pair de trading @@ -1567,7 +1592,7 @@ 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/bisq-network/bisq/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} @@ -1610,7 +1635,7 @@ popup.warning.nodeBanned=Un des noeuds {0} a été banni. 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'object de filtre de la part des noeuds source. Ceci n'est pas une situation attendue. Veuillez informer les développeurs de Haveno +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.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. @@ -1680,6 +1705,9 @@ popup.accountSigning.unsignedPubKeys.signed=Les clés publiques ont été signé popup.accountSigning.unsignedPubKeys.result.signed=Clés publiques signées popup.accountSigning.unsignedPubKeys.result.failed=Échec de la signature +popup.info.buyerAsTakerWithoutDeposit.headline=Aucun dépôt requis de la part de l'acheteur +popup.info.buyerAsTakerWithoutDeposit=Votre offre ne nécessitera pas de dépôt de sécurité ni de frais de la part de l'acheteur XMR.\n\nPour accepter votre offre, vous devez partager un mot de passe avec votre partenaire commercial en dehors de Haveno.\n\nLe mot de passe est généré automatiquement et affiché dans les détails de l'offre après sa création. + #################################################################### # Notifications #################################################################### @@ -1805,11 +1833,14 @@ navigation.support=\"Assistance\" formatter.formatVolumeLabel={0} montant{1} formatter.makerTaker=Maker comme {0} {1} / Taker comme {2} {3} +formatter.makerTaker.locked=Maker comme {0} {1} / Taker comme {2} {3} 🔒 formatter.youAreAsMaker=Vous êtes {1} {0} (maker) / Le preneur est: {3} {2} formatter.youAreAsTaker=Vous êtes: {1} {0} (preneur) / Le maker est: {3} {2} formatter.youAre=Vous êtes {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Vous êtes en train de créer un ordre pour {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Vous êtes en train de créer un ordre pour {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Vous êtes en train de créer un ordre pour {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} en tant que maker formatter.asTaker={0} {1} en tant que taker @@ -1960,9 +1991,9 @@ payment.moneyGram.info=Lors de l'utilisation de MoneyGram, l'acheteur de XMR doi payment.westernUnion.info=Lors de l'utilisation de Western Union, l'acheteur XMR doit envoyer le MTCN (numéro de suivi) et une photo du reçu par e-mail au vendeur de XMR. Le reçu doit indiquer clairement le nom complet du vendeur, la ville, le pays et le montant. L'acheteur verra ensuite s'afficher l'email du vendeur pendant le processus de transaction. payment.halCash.info=Lors de l'utilisation de HalCash, l'acheteur de XMR doit envoyer au vendeur de XMR le code HalCash par SMS depuis son téléphone portable.\n\nVeuillez vous assurer de ne pas dépasser le montant maximum que votre banque vous permet d'envoyer avec HalCash. Le montant minimum par retrait est de 10 EUR et le montant maximum est de 600 EUR. Pour les retraits récurrents, il est de 3000 EUR par destinataire par jour et 6000 EUR par destinataire par mois. Veuillez vérifier ces limites auprès de votre banque pour vous assurer qu'elles utilisent les mêmes limites que celles indiquées ici.\n\nLe montant du retrait doit être un multiple de 10 EUR car vous ne pouvez pas retirer d'autres montants à un distributeur automatique. Pendant les phases de create-offer et take-offer l'affichage de l'interface utilisateur ajustera le montant en XMR afin que le montant en euros soit correct. Vous ne pouvez pas utiliser le prix basé sur le marché, car le montant en euros varierait en fonction de l'évolution des prix.\n\nEn cas de litige, l'acheteur de XMR doit fournir la preuve qu'il a envoyé la somme en EUR. # 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://haveno.exchange/wiki/Account_limits]. +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://haveno.exchange/wiki/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. @@ -1970,6 +2001,10 @@ payment.revolut.info=Revolut nécessite le 'Nom d'utilisateur' en tant qu'ID de payment.account.revolut.addUserNameInfo={0}\nVotre compte Revolut existant ({1}) n'a pas de "Nom d'utilisateur".\nVeuillez entrer votre "Nom d'utilisateur" Revolut pour mettre à jour les données de votre compte.\nCeci n'affectera pas l'âge du compte. payment.revolut.addUserNameInfo.headLine=Mettre à jour le compte Revolut +payment.cashapp.info=Veuillez noter que Cash App présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. +payment.venmo.info=Veuillez noter que Venmo présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. +payment.paypal.info=Veuillez noter que PayPal présente un risque de rétrofacturation plus élevé que la plupart des virements bancaires. + payment.amazonGiftCard.upgrade=La méthode de paiement via carte cadeaux Amazon nécessite que le pays soit spécifié. payment.account.amazonGiftCard.addCountryInfo={0}\nVotre compte carte cadeau Amazon existant ({1}) n'a pas de pays spécifé.\nVeuillez entrer le pays de votre compte carte cadeau Amazon pour mettre à jour les données de votre compte.\nCeci n'affectera pas le statut de l'âge du compte. payment.amazonGiftCard.upgrade.headLine=Mettre à jour le compte des cartes cadeaux Amazon @@ -2003,11 +2038,14 @@ payment.f2f.city=Ville pour la rencontre en face à face payment.f2f.city.prompt=La ville sera affichée en même temps que l'ordre payment.shared.optionalExtra=Informations complémentaires facultatives payment.shared.extraInfo=Informations complémentaires -payment.shared.extraInfo.prompt=Définissez n'importe quels termes spécifiques, conditons ou détails que vous souhaiteriez voir affichés avec vos offres pour ce compte de paiement (les utilisateurs verront ces informations avant d'accepter les offres). +payment.shared.extraInfo.offer=Informations supplémentaires sur l'offre +payment.shared.extraInfo.prompt.paymentAccount=Définissez n'importe quels termes spécifiques, conditons ou détails que vous souhaiteriez voir affichés avec vos offres pour ce compte de paiement (les utilisateurs verront ces informations avant d'accepter les offres). +payment.shared.extraInfo.prompt.offer=Définissez tous les termes, conditions ou détails spéciaux que vous souhaitez afficher avec votre offre. +payment.shared.extraInfo.noDeposit=Coordonnées et conditions de l'offre payment.f2f.info=Les transactions en 'face à face' ont des règles différentes et comportent des risques différents de ceux des transactions en ligne.\n\nLes principales différences sont les suivantes:\n● Les pairs de trading doivent échanger des informations sur le lieu et l'heure de la réunion en utilisant les coordonnées de contanct qu'ils ont fournies.\n● Les pairs de trading doivent apporter leur ordinateur portable et faire la confirmation du 'paiement envoyé' et du 'paiement reçu' sur le lieu de la réunion.\n● Si un maker a des 'termes et conditions' spéciaux, il doit les indiquer dans le champ 'Informations supplémentaires' dans le compte.\n● En acceptant une offre, le taker accepte les 'termes et conditions' du maker.\n● En cas de litige, le médiateur ou l'arbitre ne peut pas beaucoup aider car il est généralement difficile d'obtenir des preuves irréfutables de ce qui s'est passé lors de la réunion. Dans ce cas, les fonds en XMR peuvent être bloqué s indéfiniment tant que les pairs ne parviennent pas à un accord.\n\nPour vous assurer de bien comprendre les spécificités des transactions 'face à face', veuillez lire les instructions et les recommandations à [LIEN:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Ouvrir la page web payment.f2f.offerbook.tooltip.countryAndCity=Pays et ville: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informations complémentaires: {0} +payment.shared.extraInfo.tooltip=Informations complémentaires: {0} payment.japan.bank=Banque payment.japan.branch=Filiale @@ -2016,7 +2054,10 @@ payment.japan.recipient=Nom payment.australia.payid=ID de paiement payment.payid=ID de paiement lié à une institution financière. Comme l'addresse email ou le téléphone portable. payment.payid.info=Un PayID, tel qu'un numéro de téléphone, une adresse électronique ou un numéro d'entreprise australien (ABN), que vous pouvez lier en toute sécurité à votre compte bancaire, votre crédit mutuel ou votre société de crédit immobilier. Vous devez avoir déjà créé un PayID auprès de votre institution financière australienne. Les institutions financières émettrices et réceptrices doivent toutes deux prendre en charge PayID. Pour plus d'informations, veuillez consulter [LIEN:https://payid.com.au/faqs/]. -payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://haveno.exchange/wiki/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). +payment.amazonGiftCard.info=Pour payer avec une carte cadeau Amazon eGift Card, vous devrez envoyer une carte cadeau Amazon eGift Card au vendeur de XMR via votre compte Amazon. \n\nHaveno indiquera l'adresse e-mail ou le numéro de téléphone du vendeur XMR où la carte cadeau doit être envoyée, et vous devrez inclure l'ID du trade dans le champ de messagerie de la carte cadeau. Veuillez consulter le wiki [LIEN:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] pour plus de détails et pour les meilleures pratiques à adopter. \n\nTrois remarques importantes :\n- essayez d'envoyer des cartes-cadeaux d'un montant inférieur ou égal à 100 USD, car Amazon est connu pour signaler les cartes-cadeaux plus importantes comme frauduleuses\n- essayez d'utiliser un texte créatif et crédible pour le message de la carte cadeau (par exemple, "Joyeux anniversaire Susan !") ainsi que l'ID du trade (et utilisez le chat du trader pour indiquer à votre pair de trading le texte de référence que vous avez choisi afin qu'il puisse vérifier votre paiement).\n- Les cartes cadeaux électroniques Amazon ne peuvent être échangées que sur le site Amazon où elles ont été achetées (par exemple, une carte cadeau achetée sur amazon.it ne peut être échangée que sur amazon.it). +payment.paysafe.info=Pour votre protection, nous déconseillons fortement d'utiliser les PINs Paysafecard pour les paiements.\n\n\ + Les transactions effectuées via des PINs ne peuvent pas être vérifiées de manière indépendante pour la résolution des litiges. En cas de problème, la récupération des fonds peut ne pas être possible.\n\n\ + Pour garantir la sécurité des transactions et la résolution des litiges, utilisez toujours des méthodes de paiement qui fournissent des preuves vérifiables. # We use constants from the code so we do not use our normal naming convention @@ -2096,7 +2137,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=eCarte cadeau Amazon # suppress inspection "UnusedProperty" @@ -2148,7 +2189,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=eCarte cadeau Amazon # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index d663400dd0..4a5d177c0a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -40,6 +40,8 @@ shared.buyMonero=Acquista monero shared.sellMonero=Vendi monero shared.buyCurrency=Acquista {0} shared.sellCurrency=Vendi {0} +shared.buyCurrency.locked=Acquista {0} 🔒 +shared.sellCurrency.locked=Vendi {0} 🔒 shared.buyingXMRWith=acquistando XMR con {0} shared.sellingXMRFor=vendendo XMR per {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Si, annulla shared.nextStep=Passo successivo shared.selectTradingAccount=Seleziona conto di trading -shared.fundFromSavingsWalletButton=Trasferisci fondi dal portafoglio Haveno +shared.fundFromSavingsWalletButton=Applica fondi dal portafoglio Haveno shared.fundFromExternalWalletButton=Apri il tuo portafoglio esterno per aggiungere fondi shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Sotto % del prezzo di mercato @@ -123,6 +125,7 @@ shared.noDateAvailable=Nessuna data disponibile shared.noDetailsAvailable=Dettagli non disponibili shared.notUsedYet=Non ancora usato shared.date=Data +shared.sendFundsDetailsWithFee=Invio: {0}\n\nAll'indirizzo di ricezione: {1}\n\nCommissione mineraria aggiuntiva: {2}\n\nSei sicuro di voler inviare questa somma? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copia negli appunti @@ -139,6 +142,7 @@ shared.addNewAccount=Aggiungi nuovo account shared.ExportAccounts=Esporta Account shared.importAccounts=Importa Account shared.createNewAccount=Crea nuovo account +shared.createNewAccountDescription=I dettagli del tuo account sono memorizzati localmente sul tuo dispositivo e condivisi solo con il tuo partner commerciale e l'arbitro se viene aperta una disputa. shared.saveNewAccount=Salva nuovo account shared.selectedAccount=Account selezionato shared.deleteAccount=Elimina account @@ -185,13 +189,14 @@ shared.total=Totale shared.totalsNeeded=Fondi richiesti shared.tradeWalletAddress=Indirizzo del portafoglio per gli scambi shared.tradeWalletBalance=Saldo del portafogli per gli scambi -shared.reserveExactAmount=Riserva solo i fondi necessari. Potrebbe essere richiesta una tassa di mining e 10 conferme (~20 minuti) prima che la tua offerta sia attiva. +shared.reserveExactAmount=Riserva solo i fondi necessari. Richiede una tassa di mining e circa 20 minuti prima che la tua offerta diventi attiva. shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=Confermo shared.openURL=Aperti {0} shared.fiat=Fiat shared.crypto=Crypto +shared.preciousMetals=Metalli Preziosi shared.all=Tutti shared.edit=Modifica shared.advancedOptions=Opzioni avanzate @@ -328,6 +333,7 @@ offerbook.createOffer=Crea offerta offerbook.takeOffer=Accetta offerta offerbook.takeOfferToBuy=Accetta l'offerta per acquistare {0} offerbook.takeOfferToSell=Accetta l'offerta per vendere {0} +offerbook.takeOffer.enterChallenge=Inserisci la passphrase dell'offerta offerbook.trader=Trader offerbook.offerersBankId=ID banca del Maker (BIC/SWIFT): {0} offerbook.offerersBankName=Nome della banca del Maker: {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=Sede accettata dei paesi bancari (acquirente offerbook.availableOffers=Offerte disponibili offerbook.filterByCurrency=Filtra per valuta offerbook.filterByPaymentMethod=Filtra per metodo di pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Offerte che corrispondono ai miei account +offerbook.filterNoDeposit=Nessun deposito +offerbook.noDepositOffers=Offerte senza deposito (passphrase richiesta) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Questo account è stato verificato e {0} offerbook.timeSinceSigning.info.arbitrator=firmato da un arbitro e può firmare account peer @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned= \nl'account è stato bannato offerbook.timeSinceSigning.daysSinceSigning={0} giorni offerbook.timeSinceSigning.daysSinceSigning.long={0} dalla firma offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.buyXmrWith=Compra XMR con: +offerbook.sellXmrFor=Vendi XMR per: offerbook.timeSinceSigning.help=Quando completi correttamente un'operazione con un peer che ha un account di pagamento firmato, il tuo account di pagamento viene firmato.\n{0} giorni dopo, il limite iniziale di {1} viene alzato e il tuo account può firmare account di pagamento di altri peer. offerbook.timeSinceSigning.notSigned=Non ancora firmato @@ -362,6 +372,7 @@ offerbook.nrOffers=N. di offerte: {0} offerbook.volume={0} (min - max) 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.createNewOffer=Crea offerta per {0} {1} offerbook.createOfferToBuy=Crea una nuova offerta per comprare {0} offerbook.createOfferToSell=Crea una nuova offerta per vendere {0} @@ -452,7 +463,12 @@ createOffer.placeOfferButton=Revisione: piazza l'offerta a {0} monero createOffer.createOfferFundWalletInfo.headline=Finanzia la tua offerta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n -createOffer.createOfferFundWalletInfo.msg=Devi depositare {0} a questa offerta.\n\nTali fondi sono riservati nel tuo portafoglio locale e verranno bloccati nell'indirizzo di deposito multisig una volta che qualcuno accetta la tua offerta.\n\nL'importo è la somma di:\n{1} - Il tuo deposito cauzionale: {2}\n- Commissione di scambio: {3}\n- Commissione di mining: {4}\n\nPuoi scegliere tra due opzioni quando finanzi il tuo scambio:\n- Usa il tuo portafoglio Haveno (comodo, ma le transazioni possono essere collegabili) OPPURE\n- Effettua il trasferimento da un portafoglio esterno (potenzialmente più privato)\n\nVedrai tutte le opzioni di finanziamento e i dettagli dopo aver chiuso questo popup. +createOffer.createOfferFundWalletInfo.msg=Devi depositare {0} per questa offerta.\n\n\ + Questi fondi sono riservati nel tuo portafoglio locale e verranno bloccati in un portafoglio multisig una volta che qualcuno accetta la tua offerta.\n\n\ + L'importo è la somma di:\n\ + {1}\ + - Il tuo deposito di sicurezza: {2}\n\ + - Tassa di trading: {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=Si è verificato un errore durante l'immissione dell'offerta:\n\n{0}\n\nNon sono ancora usciti fondi dal tuo portafoglio.\nRiavvia l'applicazione e controlla la connessione di rete. @@ -474,7 +490,10 @@ createOffer.setDepositAsBuyer=Imposta il mio deposito cauzionale come acquirente createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Il deposito cauzionale dell'acquirente sarà {0} createOffer.securityDepositInfoAsBuyer=Il tuo deposito cauzionale come acquirente sarà {0} -createOffer.minSecurityDepositUsed=Viene utilizzato il minimo deposito cauzionale dell'acquirente +createOffer.minSecurityDepositUsed=Il deposito di sicurezza minimo è utilizzato +createOffer.buyerAsTakerWithoutDeposit=Nessun deposito richiesto dal compratore (protetto da passphrase) +createOffer.myDeposit=Il mio deposito di sicurezza (%) +createOffer.myDepositInfo=Il tuo deposito di sicurezza sarà {0} #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=Totale commissioni di mining takeOffer.fundsBox.takeOfferSpinnerInfo=Accettare l'offerta: {0} takeOffer.fundsBox.paymentLabel=Scambia Haveno con ID {0} takeOffer.fundsBox.fundsStructure=({0} deposito cauzionale, {1} commissione commerciale, {2} commissione mineraria) +takeOffer.fundsBox.noFundingRequiredTitle=Nessun finanziamento richiesto +takeOffer.fundsBox.noFundingRequiredDescription=Ottieni la passphrase dell'offerta dal venditore fuori da Haveno per accettare questa offerta. takeOffer.success.headline=Hai accettato con successo un'offerta. takeOffer.success.info=Puoi vedere lo stato del tuo scambio su \"Portafoglio/Scambi aperti\". takeOffer.error.message=Si è verificato un errore durante l'accettazione dell'offerta.\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=Non puoi accettare questa offerta poiché utilizz takeOffer.takeOfferFundWalletInfo.headline=Finanzia il tuo scambio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Importo di scambio: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Devi depositare {0} per accettare questa offerta.\n\nL'importo è la somma de:\n{1} - Il tuo deposito cauzionale: {2}\n- La commissione di trading: {3}\n- I costi di mining: {4}\n\nPuoi scegliere tra due opzioni quando finanzi il tuo scambio:\n- Usare il tuo portafoglio Haveno (comodo, ma le transazioni possono essere collegabili) OPPURE\n- Trasferimento da un portafoglio esterno (potenzialmente più privato)\n\nVedrai tutte le opzioni di finanziamento e i dettagli dopo aver chiuso questo popup. +takeOffer.takeOfferFundWalletInfo.msg=Devi depositare {0} per accettare questa offerta.\n\nL'importo è la somma di:\n{1}- Il tuo deposito di sicurezza: {2}\n- Commissione di trading: {3} takeOffer.alreadyPaidInFunds=Se hai già pagato in fondi puoi effettuare il ritiro nella schermata \"Fondi/Invia fondi\". takeOffer.paymentInfo=Informazioni sul pagamento takeOffer.setAmountPrice=Importo stabilito @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=La transazione di deposito è stata pubblicata.\n {0} deve attendere almeno una conferma dalla blockchain prima di avviare il pagamento. +portfolio.pending.step1.info.you=La transazione di deposito è stata pubblicata.\nDevi aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. +portfolio.pending.step1.info.buyer=La transazione di deposito è stata pubblicata.\nL'acquirente XMR deve aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. portfolio.pending.step1.warn=La transazione di deposito non è ancora confermata. Questo accade raramente e nel caso in cui la commissione di transazione di un trader proveniente da un portafoglio esterno è troppo bassa. portfolio.pending.step1.openForDispute=La transazione di deposito non è ancora confermata. Puoi attendere più a lungo o contattare il mediatore per ricevere assistenza. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDopo ave # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Invia {0} tramite \"Vaglia Postale Statunitense\" al venditore XMR.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1032,6 +1054,8 @@ settings.net.p2pHeader=Rete Haveno settings.net.onionAddressLabel=Il mio indirizzo onion settings.net.xmrNodesLabel=Usa nodi Monero personalizzati settings.net.moneroPeersLabel=Peer connessi +settings.net.connection=Connessione +settings.net.connected=Connesso settings.net.useTorForXmrJLabel=Usa Tor per la rete Monero settings.net.moneroNodesLabel=Nodi Monero a cui connettersi settings.net.useProvidedNodesRadio=Usa i nodi Monero Core forniti @@ -1451,6 +1475,7 @@ offerDetailsWindow.confirm.maker=Conferma: Piazza l'offerta a {0} monero offerDetailsWindow.confirm.taker=Conferma: Accetta l'offerta a {0} monero offerDetailsWindow.creationDate=Data di creazione offerDetailsWindow.makersOnion=Indirizzo .onion del maker +offerDetailsWindow.challenge=Passphrase dell'offerta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1491,7 +1516,7 @@ tacWindow.disagree=Non accetto ed esco tacWindow.arbitrationSystem=Risoluzione disputa tradeDetailsWindow.headline=Scambio -tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato: +tradeDetailsWindow.disputedPayoutTxId=ID transazione di pagamento contestato tradeDetailsWindow.tradeDate=Data di scambio tradeDetailsWindow.txFee=Commissione di mining tradeDetailsWindow.tradePeersOnion=Indirizzi onion peer di trading @@ -1564,7 +1589,7 @@ popup.headline.error=Errore popup.doNotShowAgain=Non mostrare di nuovo popup.reportError.log=Apri file di registro popup.reportError.gitHub=Segnala sugli errori di GitHub -popup.reportError={0}\n\nPer aiutarci a migliorare il software, segnala questo errore aprendo un nuova segnalazione su https://github.com/bisq-network/bisq/issues.\nIl messaggio di errore sopra verrà copiato negli appunti quando si fa clic su uno dei pulsanti di seguito.\nFaciliterà il debug se includi il file haveno.log premendo "Apri file di registro", salvando una copia e allegandolo alla tua segnalazione di bug.\n  +popup.reportError={0}\n\nPer aiutarci a migliorare il software, segnala questo errore aprendo un nuova segnalazione su https://github.com/haveno-dex/haveno/issues.\nIl messaggio di errore sopra verrà copiato negli appunti quando si fa clic su uno dei pulsanti di seguito.\nFaciliterà il debug se includi il file haveno.log premendo "Apri file di registro", salvando una copia e allegandolo alla tua segnalazione di bug.\n  popup.error.tryRestart=Prova a riavviare l'applicazione e controlla la connessione di rete per vedere se riesci a risolvere il problema. popup.error.takeOfferRequestFailed=Si è verificato un errore quando qualcuno ha tentato di accettare una delle tue offerte:\n{0} @@ -1607,6 +1632,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ripetitore di prezzo popup.warning.seed=seme popup.warning.mandatoryUpdate.trading=Si prega di aggiornare Haveno all'ultima versione. È stato rilasciato un aggiornamento obbligatorio che disabilita il trading per le vecchie versioni. Per ulteriori informazioni, consultare il forum Haveno. +popup.warning.noFilter=Non abbiamo ricevuto un oggetto filtro dai nodi seme. Si prega di informare gli amministratori di rete di registrare un oggetto filtro. popup.warning.burnXMR=Questa transazione non è possibile, poiché le commissioni di mining di {0} supererebbero l'importo da trasferire di {1}. Attendi fino a quando le commissioni di mining non saranno nuovamente basse o fino a quando non avrai accumulato più XMR da trasferire. popup.warning.openOffer.makerFeeTxRejected=La commissione della transazione del creatore dell'offerta con ID {0} è stata rifiutata dalla rete Monero.\nTransazione ID={1}.\nL'offerta è stata rimossa per evitare ulteriori problemi.\nVai su \"Impostazioni/Informazioni di rete\" ed esegui una risincronizzazione SPV.\nPer ulteriore assistenza, contattare il canale di supporto Haveno nel team di Haveno Keybase. @@ -1674,6 +1700,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nessun deposito richiesto dal compratore +popup.info.buyerAsTakerWithoutDeposit=La tua offerta non richiederà un deposito di sicurezza o una commissione da parte dell'acquirente XMR.\n\nPer accettare la tua offerta, devi condividere una passphrase con il tuo partner commerciale al di fuori di Haveno.\n\nLa passphrase viene generata automaticamente e mostrata nei dettagli dell'offerta dopo la creazione. + #################################################################### # Notifications #################################################################### @@ -1799,11 +1828,14 @@ navigation.support=\"Supporto\" formatter.formatVolumeLabel={0} importo{1} formatter.makerTaker=Maker come {0} {1} / Taker come {2} {3} +formatter.makerTaker.locked=Maker come {0} {1} / Taker come {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Sei {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Stai creando un'offerta per {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Stai creando un'offerta per {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Stai creando un'offerta per {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Stai creando un'offerta per {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} come maker formatter.asTaker={0} {1} come taker @@ -1946,17 +1978,15 @@ payment.accountType=Tipologia conto payment.checking=Verifica payment.savings=Risparmi payment.personalId=ID personale -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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. payment.halCash.info=Quando utilizza HalCash, l'acquirente XMR deve inviare al venditore XMR il codice HalCash tramite un messaggio di testo dal proprio telefono cellulare.\n\nAssicurati di non superare l'importo massimo che la tua banca ti consente di inviare con HalCash. L'importo minimo per prelievo è di 10 EURO, l'importo massimo è di 600 EURO. Per prelievi ripetuti è di 3000 EURO per destinatario al giorno e 6000 EURO per destintario al mese. Verifica i limiti con la tua banca per accertarti che utilizzino gli stessi limiti indicati qui.\n\nL'importo del prelievo deve essere un multiplo di 10 EURO in quanto non è possibile prelevare altri importi da un bancomat. L'interfaccia utente nella schermata di creazione offerta e accettazione offerta modificherà l'importo XMR in modo che l'importo in EURO sia corretto. Non è possibile utilizzare il prezzo di mercato poiché l'importo in EURO cambierebbe al variare dei prezzi.\n\nIn caso di controversia, l'acquirente XMR deve fornire la prova di aver inviato gli EURO. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. @@ -1964,12 +1994,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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. +payment.venmo.info=Si prega di notare che Venmo ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. +payment.paypal.info=Si prega di notare che PayPal ha un rischio di chargeback più elevato rispetto alla maggior parte dei bonifici bancari. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=Informazioni di contatto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informazioni di contatto @@ -1978,11 +2010,13 @@ payment.f2f.city=Città per l'incontro 'Faccia a faccia' payment.f2f.city.prompt=La città verrà visualizzata con l'offerta payment.shared.optionalExtra=Ulteriori informazioni opzionali payment.shared.extraInfo=Informazioni aggiuntive -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=Informazioni aggiuntive sull'offerta +payment.shared.extraInfo.prompt.paymentAccount=Definisci eventuali termini, condizioni o dettagli speciali che desideri vengano visualizzati con le tue offerte per questo account di pagamento (gli utenti vedranno queste informazioni prima di accettare le offerte). +payment.shared.extraInfo.prompt.offer=Definisci eventuali termini, condizioni o dettagli speciali che desideri mostrare con la tua offerta. +payment.shared.extraInfo.noDeposit=Dettagli di contatto e termini dell'offerta payment.f2f.info.openURL=Apri sito web payment.f2f.offerbook.tooltip.countryAndCity=Paese e città: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Ulteriori informazioni: {0} +payment.shared.extraInfo.tooltip=Ulteriori informazioni: {0} payment.japan.bank=Banca payment.japan.branch=Filiale @@ -1991,7 +2025,10 @@ 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://haveno.exchange/wiki/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. # We use constants from the code so we do not use our normal naming convention @@ -2071,7 +2108,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2123,7 +2160,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 372906c2a3..3514d37343 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -40,6 +40,8 @@ shared.buyMonero=ビットコインを買う shared.sellMonero=ビットコインを売る shared.buyCurrency={0}を買う shared.sellCurrency={0}を売る +shared.buyCurrency.locked={0}を買う 🔒 +shared.sellCurrency.locked={0}を売る 🔒 shared.buyingXMRWith=XMRを{0}で買う shared.sellingXMRFor=XMRを{0}で売る shared.buyingCurrency={0}を購入中 (XMRを売却中) @@ -103,7 +105,7 @@ shared.faq=FAQを参照する shared.yesCancel=はい、取り消します shared.nextStep=次へ shared.selectTradingAccount=取引アカウントを選択 -shared.fundFromSavingsWalletButton=Havenoウォレットから資金を移動する +shared.fundFromSavingsWalletButton=Havenoウォレットから資金を適用 shared.fundFromExternalWalletButton=外部のwalletを開く shared.openDefaultWalletFailed=ビットコインウォレットのアプリを開けませんでした。インストールされているか確認して下さい。 shared.belowInPercent=市場価格から%以下 @@ -123,6 +125,7 @@ shared.noDateAvailable=日付がありません shared.noDetailsAvailable=詳細不明 shared.notUsedYet=未使用 shared.date=日付 +shared.sendFundsDetailsWithFee=送信中: {0}\n\n受取アドレス: {1}\n\n追加のマイナー手数料: {2}\n\nこの金額を送信してもよろしいですか? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Havenoがこのトランザクションはダストの最小閾値以下のおつりアウトプットを生じることを検出しました(それにしたがって、ビットコインのコンセンサス・ルールによって許されない)。代わりに、その ({0} satoshi{1}) のダストはマイニング手数料に追加されます。\n\n\n shared.copyToClipboard=クリップボードにコピー @@ -139,6 +142,7 @@ shared.addNewAccount=アカウントを追加 shared.ExportAccounts=アカウントをエクスポート shared.importAccounts=アカウントをインポート shared.createNewAccount=新しいアカウントを作る +shared.createNewAccountDescription=あなたのアカウント詳細は、デバイスにローカルに保存され、取引相手および紛争が発生した場合には仲裁人とのみ共有されます。 shared.saveNewAccount=新しいアカウントを保存する shared.selectedAccount=選択したアカウント shared.deleteAccount=アカウントを削除 @@ -185,13 +189,14 @@ shared.total=合計 shared.totalsNeeded=必要な資金 shared.tradeWalletAddress=トレードウォレットアドレス shared.tradeWalletBalance=トレードウォレット残高 -shared.reserveExactAmount=必要な資金のみを予約してください。提供が有効になる前に、マイニング手数料と10回の確認(約20分)が必要な場合があります。 +shared.reserveExactAmount=必要な資金のみを予約してください。オファーが公開されるまでにマイニング手数料と約20分が必要です。 shared.makerTxFee=メイカー: {0} shared.takerTxFee=テイカー: {0} shared.iConfirm=確認します shared.openURL={0} をオープン shared.fiat=法定通貨 shared.crypto=暗号通貨 +shared.preciousMetals=貴金属 shared.all=全て shared.edit=編集 shared.advancedOptions=高度なオプション @@ -328,6 +333,7 @@ offerbook.createOffer=オファーを作る offerbook.takeOffer=オファーを受ける offerbook.takeOfferToBuy={0}購入オファーを受ける offerbook.takeOfferToSell={0}売却オファーを受ける +offerbook.takeOffer.enterChallenge=オファーのパスフレーズを入力してください offerbook.trader=取引者 offerbook.offerersBankId=メイカーの銀行ID (BIC/SWIFT): {0} offerbook.offerersBankName=メーカーの銀行名: {0} @@ -338,6 +344,8 @@ offerbook.availableOffers=利用可能なオファー offerbook.filterByCurrency=通貨でフィルター offerbook.filterByPaymentMethod=支払い方法でフィルター offerbook.matchingOffers=アカウントと一致するオファー +offerbook.filterNoDeposit=デポジットなし +offerbook.noDepositOffers=預金不要のオファー(パスフレーズ必須) offerbook.timeSinceSigning=アカウント情報 offerbook.timeSinceSigning.info=このアカウントは認証されまして、{0} offerbook.timeSinceSigning.info.arbitrator=調停人に署名されました。ピアアカウントも署名できます @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=このアカウントは禁止されま offerbook.timeSinceSigning.daysSinceSigning={0}日 offerbook.timeSinceSigning.daysSinceSigning.long=署名する後から {0} offerbook.xmrAutoConf=自動確認は有効されますか? +offerbook.buyXmrWith=XMRを購入: +offerbook.sellXmrFor=XMRを売る: offerbook.timeSinceSigning.help=署名された支払いアカウントを持っているピアと成功にトレードすると、自身の支払いアカウントも署名されることになります。\n{0} 日後に、{1} という初期の制限は解除され、他のピアの支払いアカウントを署名できるようになります。 offerbook.timeSinceSigning.notSigned=まだ署名されていません @@ -362,6 +372,7 @@ offerbook.nrOffers=オファー数: {0} offerbook.volume={0} (下限 - 上限) offerbook.deposit=XMRの敷金(%) offerbook.deposit.help=トレードを保証するため、両方の取引者が支払う敷金。トレードが完了されたら、返還されます。 +offerbook.createNewOffer={0} {1}にオファーを作成する offerbook.createOfferToBuy={0} を購入するオファーを作成 offerbook.createOfferToSell={0} を売却するオファーを作成 @@ -453,7 +464,12 @@ createOffer.placeOfferButton=再確認: ビットコインを{0}オファーを createOffer.createOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 取引額: {0}\n -createOffer.createOfferFundWalletInfo.msg=このオファーに対して {0} のデポジットを送金する必要があります。\n\nこの資金はあなたのローカルウォレットに予約済として保管され、オファーが受け入れられた時にマルチシグデポジットアドレスに移動しロックされます。\n\n金額の合計は以下の通りです\n{1} - セキュリティデポジット: {2}\n- 取引手数料: {3}\n- マイニング手数料: {4}\n\nこのオファーにデポジットを送金するには、以下の2つの方法があります。\n- Havenoウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 +createOffer.createOfferFundWalletInfo.msg=このオファーには {0} をデポジットする必要があります。\n\n\ + この資金はあなたのローカルウォレットに予約され、誰かがあなたのオファーを受け入れるとマルチシグウォレットにロックされます。\n\n\ + 金額は以下の合計です:\n\ + {1}\ + - あなたの保証金: {2}\n\ + - 取引手数料: {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=オファーを出す時にエラーが発生しました:\n\n{0}\n\nウォレットにまだ資金がありません。\nアプリケーションを再起動してネットワーク接続を確認してください。 @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=購入時のセキュリティデポジット (%) createOffer.setDepositForBothTraders=両方の取引者の保証金を設定する(%) createOffer.securityDepositInfo=あなたの買い手のセキュリティデポジットは{0}です createOffer.securityDepositInfoAsBuyer=あなたの購入時のセキュリティデポジットは{0}です -createOffer.minSecurityDepositUsed=最小値の買い手の保証金は使用されます +createOffer.minSecurityDepositUsed=最低セキュリティデポジットが使用されます +createOffer.buyerAsTakerWithoutDeposit=購入者に保証金は不要(パスフレーズ保護) +createOffer.myDeposit=私の保証金(%) +createOffer.myDepositInfo=あなたのセキュリティデポジットは{0}です #################################################################### @@ -499,6 +518,8 @@ takeOffer.fundsBox.networkFee=合計マイニング手数料 takeOffer.fundsBox.takeOfferSpinnerInfo=オファーを受け入れる: {0} takeOffer.fundsBox.paymentLabel=次のIDとのHavenoトレード: {0} takeOffer.fundsBox.fundsStructure=({0} セキュリティデポジット, {1} 取引手数料, {2}マイニング手数料) +takeOffer.fundsBox.noFundingRequiredTitle=資金は必要ありません +takeOffer.fundsBox.noFundingRequiredDescription=このオファーを受けるには、Haveno外で売り手からオファーパスフレーズを取得してください。 takeOffer.success.headline=オファー受け入れに成功しました takeOffer.success.info=あなたのトレード状態は「ポートフォリオ/オープントレード」で見られます takeOffer.error.message=オファーの受け入れ時にエラーが発生しました。\n\n{0} @@ -509,7 +530,7 @@ takeOffer.noPriceFeedAvailable=そのオファーは市場価格に基づくパ takeOffer.takeOfferFundWalletInfo.headline=あなたのオファーへ入金 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount= - 取引額: {0}\n -takeOffer.takeOfferFundWalletInfo.msg=このオファーに対して {0} のデポジットを送金する必要があります。\n\n金額の合計は以下の通りです\n{1} - セキュリティデポジット: {2}\n- 取引手数料: {3}\n- マイニング手数料: {4}\n\nこのオファーにデポジットを送金するには、以下の2つの方法があります。\n- Havenoウォレットを使う (便利ですがトランザクションが追跡される可能性があります)\nまたは\n- 外部のウォレットから送金する (機密性の高い方法です)\n\nこのポップアップを閉じると全ての送金方法について詳細な情報が表示されます。 +takeOffer.takeOfferFundWalletInfo.msg=このオファーを受けるには、{0} を預ける必要があります。\n\n金額は以下の合計です:\n{1}- あなたの保証金: {2}\n- 取引手数料: {3} takeOffer.alreadyPaidInFunds=あなたがすでに資金を支払っている場合は「資金/送金する」画面でそれを出金することができます。 takeOffer.paymentInfo=支払い情報 takeOffer.setAmountPrice=金額を設定 @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=サービスリクエストにはエラ # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=サービスは失敗を返しました。自動確認できません。 -portfolio.pending.step1.info=デポジットトランザクションが発行されました。\n{0}は、支払いを開始する前に少なくとも1つのブロックチェーンの承認を待つ必要があります。 +portfolio.pending.step1.info.you=入金トランザクションが公開されました。\n支払いが開始されるまで、10回の確認(約20分)を待つ必要があります。 +portfolio.pending.step1.info.buyer=入金トランザクションが公開されました。\nXMRの購入者は、支払いを開始する前に10回の確認(約20分)を待つ必要があります。 portfolio.pending.step1.warn=デポジットトランザクションがまだ承認されていません。外部ウォレットからの取引者の資金調達手数料が低すぎるときには、例外的なケースで起こるかもしれません。 portfolio.pending.step1.openForDispute=デポジットトランザクションがまだ承認されていません。もう少し待つか、助けを求めて調停人に連絡できます。 @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要な要件: \n支払いが # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal={0}を「米国の郵便為替」でXMRの売り手に送付してください。\n\n # suppress inspection "TrailingSpacesInProperty" -portfolio.pending.step2_buyer.payByMail=\"郵送で現金\"で、{0}をXMR売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはHavenoのWikiを参照:[HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail]\n +portfolio.pending.step2_buyer.payByMail=\"郵送で現金\"で、{0}をXMR売り手に送って下さい。詳細な指示はトレード契約書に書いてあります、そして分からない点があれば取引者チャットで質問できます。「郵送で現金」について詳しくはHavenoのWikiを参照:[HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail]\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=特定された支払い方法で{0}をXMRの売り手に支払ってお願いします。売り手のアカウント詳細は次の画面に表示されます。\n\n # suppress inspection "TrailingSpacesInProperty" @@ -798,11 +820,11 @@ 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/bisq-network/support/issues]\n\nこのトレードを「失敗トレード」へ送れます。 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\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] +portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=トレードプロトコルは問題に遭遇しました。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=トレードプロトコルは深刻な問題に遭遇しました。\n\n{0}\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 portfolio.pending.failedTrade.txChainValid.moveToFailed=トレードプロトコルは問題に遭遇しました。\n\n{0}\n\nトレードのトランザクションは公開され、資金はロックされました。絶対に確信している場合のみにトレードを「失敗トレード」へ送りましょう。問題を解決できる選択肢に邪魔する可能性はあります。\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 @@ -1033,6 +1055,8 @@ settings.net.p2pHeader=Havenoネットワーク settings.net.onionAddressLabel=私のonionアドレス settings.net.xmrNodesLabel=任意のモネロノードを使う settings.net.moneroPeersLabel=接続されたピア +settings.net.connection=接続 +settings.net.connected=接続されました settings.net.useTorForXmrJLabel=MoneroネットワークにTorを使用 settings.net.moneroNodesLabel=接続するMoneroノード: settings.net.useProvidedNodesRadio=提供されたMonero Core ノードを使う @@ -1452,6 +1476,7 @@ offerDetailsWindow.confirm.maker=承認: ビットコインを{0}オファーを offerDetailsWindow.confirm.taker=承認: ビットコインを{0}オファーを受ける offerDetailsWindow.creationDate=作成日 offerDetailsWindow.makersOnion=メイカーのonionアドレス +offerDetailsWindow.challenge=オファーパスフレーズ qRCodeWindow.headline=QRコード qRCodeWindow.msg=外部ウォレットからHavenoウォレットへ送金するのに、このQRコードを利用して下さい。 @@ -1492,7 +1517,7 @@ tacWindow.disagree=同意せずにに終了 tacWindow.arbitrationSystem=紛争解決 tradeDetailsWindow.headline=トレード -tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID: +tradeDetailsWindow.disputedPayoutTxId=係争中の支払い取引ID tradeDetailsWindow.tradeDate=取引日 tradeDetailsWindow.txFee=マイニング手数料 tradeDetailsWindow.tradePeersOnion=トレード相手のonionアドレス @@ -1565,7 +1590,7 @@ popup.headline.error=エラー popup.doNotShowAgain=次回から表示しない popup.reportError.log=ログファイルを開く popup.reportError.gitHub=GitHub issue trackerに報告 -popup.reportError={0}\n\nソフトウェアの改善に役立てるため、https://github.com/bisq-network/bisq/issues で新しい issue を開いてこのバグを報告してください。\n下のボタンのいずれかをクリックすると、上記のエラーメッセージがクリップボードにコピーされます。\n「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるhaveno.logファイル含めると、デバッグが容易になります。 +popup.reportError={0}\n\nソフトウェアの改善に役立てるため、https://github.com/haveno-dex/haveno/issues で新しい issue を開いてこのバグを報告してください。\n下のボタンのいずれかをクリックすると、上記のエラーメッセージがクリップボードにコピーされます。\n「ログファイルを開く」を押して、コピーを保存し、バグレポートに添付されるhaveno.logファイル含めると、デバッグが容易になります。 popup.error.tryRestart=アプリケーションを再起動し、ネットワーク接続を確認して問題を解決できるかどうかを確認してください。 popup.error.takeOfferRequestFailed=誰かがあなたのいずれかのオファーを受けようと時にエラーが発生しました:\n{0} @@ -1608,6 +1633,7 @@ popup.warning.nodeBanned={0}ノードの1つが禁止されました。 popup.warning.priceRelay=価格中継 popup.warning.seed=シード popup.warning.mandatoryUpdate.trading=最新のHavenoバージョンに更新してください。古いバージョンのトレードを無効にする必須の更新プログラムがリリースされました。詳細については、Havenoフォーラムをご覧ください。 +popup.warning.noFilter=シードノードからフィルターオブジェクトを受け取っていません。ネットワーク管理者にフィルターオブジェクトを登録するように通知してください。 popup.warning.burnXMR={0}のマイニング手数料が{1}の送金額を超えるため、このトランザクションは利用不可です。マイニング手数料が再び低くなるか、送金するXMRがさらに蓄積されるまでお待ちください。 popup.warning.openOffer.makerFeeTxRejected=ID{0}で識別されるオファーのためのメイカー手数料トランザクションがビットコインネットワークに拒否されました。\nトランザクションID= {1} 。\n更なる問題を避けるため、そのオファーは削除されました。\n\"設定/ネットワーク情報\"を開いてSPV再同期を行って下さい。\nさらにサポートを受けるため、Haveno Keybaseチームのサポートチャンネルに連絡して下さい。 @@ -1677,6 +1703,9 @@ popup.accountSigning.unsignedPubKeys.signed=パブリックキーは署名され popup.accountSigning.unsignedPubKeys.result.signed=署名されたパブリックキー popup.accountSigning.unsignedPubKeys.result.failed=署名が失敗しました +popup.info.buyerAsTakerWithoutDeposit.headline=購入者による保証金は不要 +popup.info.buyerAsTakerWithoutDeposit=あなたのオファーには、XMR購入者からのセキュリティデポジットや手数料は必要ありません。\n\nオファーを受け入れるには、Haveno外で取引相手とパスフレーズを共有する必要があります。\n\nパスフレーズは自動的に生成され、作成後にオファーの詳細に表示されます。 + #################################################################### # Notifications #################################################################### @@ -1802,11 +1831,14 @@ navigation.support=「サポート」 formatter.formatVolumeLabel={0} 額{1} formatter.makerTaker=メイカーは{0} {1} / テイカーは{2} {3} +formatter.makerTaker.locked=メイカーは{0} {1} / テイカーは{2} {3} 🔒 formatter.youAreAsMaker=あなたは:{1} {0}(メイカー) / テイカーは:{3} {2} formatter.youAreAsTaker=あなたは:{1} {0}(テイカー) / メイカーは{3} {2} formatter.youAre=あなたは{0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=あなたはオファーを{0} {1}に作成中です +formatter.youAreCreatingAnOffer.traditional.locked=あなたはオファーを{0} {1}に作成中です 🔒 formatter.youAreCreatingAnOffer.crypto=あなたはオファーを{0} {1} ({2} {3})に作成中です +formatter.youAreCreatingAnOffer.crypto.locked=あなたはオファーを{0} {1} ({2} {3})に作成中です 🔒 formatter.asMaker={0} {1}のメイカー formatter.asTaker={0} {1}のテイカー @@ -1949,17 +1981,15 @@ payment.accountType=口座種別 payment.checking=当座口座 payment.savings=普通口座 payment.personalId=個人ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelleは他の銀行を介して利用するとよりうまくいく送金サービスです。\n\n1. あなたの銀行がZelleと協力するか(そして利用の方法)をここから確認して下さい: [HYPERLINK:https://www.zellepay.com/get-started]\n\n2. 送金制限に注意して下さい。制限は銀行によって異なり、1日、1週、1月当たりの制限に分けられていることが多い。\n\n3. 銀行がZelleと協力しない場合でも、Zelleのモバイルアプリ版を使えますが、送金制限ははるかに低くなります。\n\n4. Havenoアカウントで特定される名前は必ずZelleアカウントと銀行口座に特定される名前と合う必要があります。\n\nトレード契約書とおりにZelleトランザクションを完了できなければ、一部(あるいは全て)のセキュリティデポジットを失う可能性はあります。\n\nZelleにおいてやや高い支払取り消しリスクがあるので、売り手はメールやSMSで無署名買い手に連絡して、Havenoに特定されるZelleアカウントの所有者かどうかを確かめるようにおすすめします。 payment.fasterPayments.newRequirements.info=「Faster Payments」で送金する場合、銀行が受信者の姓名を確認するケースが最近多くなりました。現在の「Faster Payments」アカウントは姓名を特定しません。\n\nこれからの{0}買い手に姓名を提供するため、Haveno内に新しい「Faster Payments」アカウントを作成するのを検討して下さい。\n\n新しいアカウントを作成すると、完全に同じ分類コード、アカウントの口座番号、そしてアカウント年齢検証ソルト値を古いアカウントから新しいアカウントにコピーして下さい。こうやって現在のアカウントの年齢そして署名状況は維持されます。 payment.moneyGram.info=MoneyGramを使用する場合、XMRの買い手は認証番号と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 payment.westernUnion.info=Western Unionを使用する場合、XMRの買い手はMTCN(追跡番号)と領収書の写真をEメールでXMRの売り手に送信する必要があります。領収書には、売り手の氏名、市区町村、国、金額を明確に記載する必要があります。トレードプロセスにて、売り手のEメールは買い手に表示されます。 payment.halCash.info=HalCashを使用する場合、XMRの買い手は携帯電話からのテキストメッセージを介してXMRの売り手にHalCashコードを送信する必要があります。\n\n銀行がHalCashで送金できる最大額を超えないようにしてください。 1回の出金あたりの最小金額は10EURで、最大金額は600EURです。繰り返し出金する場合は、1日に受取人1人あたり3000EUR、1ヶ月に受取人1人あたり6000EURです。あなたの銀行でも、ここに記載されているのと同じ制限を使用しているか、これらの制限を銀行と照合して確認してください。\n\n出金額は10の倍数EURでなければ、ATMから出金できません。 オファーの作成画面およびオファー受け入れ画面のUIは、EUR金額が正しくなるようにXMR金額を調整します。価格の変化とともにEURの金額は変化するため、市場ベースの価格を使用することはできません。\n\n係争が発生した場合、XMRの買い手はEURを送ったという証明を提出する必要があります。 # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Havenoは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://haveno.exchange/wiki/Account_limits] 。 +payment.limits.info=すべての銀行振込にはある程度の支払取り消しのリスクがあることに気を付けて下さい。\n\nこのリスクを軽減するために、Havenoは使用する支払い方法での支払取り消しリスクの推定レベルに基づいてトレードごとの制限を設定します。\n\n現在使用する支払い方法では、トレードごとの売買制限は{2}です。\n\n制限は各トレードの量のみに適用されることに注意して下さい。トレードできる合計回数には制限はありません。\n\n詳しくはWikiを調べて下さい [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits] 。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Havenoはこの支払いアカウントに下記の2つの要因に基づいてトレードごとの制限を設定します。\n\n1.使用する支払い方法での支払取り消しリスクの推定レベル\n2.アカウントの署名状況\n\nこの支払いアカウントはまだ無署名ですので、トレードごとに{0}の買い制限があります。 アカウントが署名される後、トレードごとの制限は以下のように成長します:\n\n●署名の前、そして署名から30日間までに、1トレードあたりの買い制限は{0}になります\n●署名から30日間後に、1トレードあたりの買い制限は{1}になります\n●署名から60日間後に、1トレードあたりの買い制限は{2}になります\n\n売り制限は署名状況に関係がありません。現在のところ、1トレードあたりに{2}を売ることができます。\n\n制限は各トレードの量のみに適用されることに注意して下さい。取引できる合計回数には制限はありません。\n\n詳しくは: [HYPERLINK:https://haveno.exchange/wiki/Account_limits] +payment.limits.info.withSigning=支払取り消しのリスクを軽減するために、Havenoはこの支払いアカウントに下記の2つの要因に基づいてトレードごとの制限を設定します。\n\n1.使用する支払い方法での支払取り消しリスクの推定レベル\n2.アカウントの署名状況\n\nこの支払いアカウントはまだ無署名ですので、トレードごとに{0}の買い制限があります。 アカウントが署名される後、トレードごとの制限は以下のように成長します:\n\n●署名の前、そして署名から30日間までに、1トレードあたりの買い制限は{0}になります\n●署名から30日間後に、1トレードあたりの買い制限は{1}になります\n●署名から60日間後に、1トレードあたりの買い制限は{2}になります\n\n売り制限は署名状況に関係がありません。現在のところ、1トレードあたりに{2}を売ることができます。\n\n制限は各トレードの量のみに適用されることに注意して下さい。取引できる合計回数には制限はありません。\n\n詳しくは: [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits] payment.cashDeposit.info=あなたの銀行が他の人の口座に現金入金を送ることを許可していることを確認してください。たとえば、Bank of America と Wells Fargo では、こうした預金は許可されなくなりました。 @@ -1967,6 +1997,10 @@ payment.revolut.info=以前の場合と違って、Revolutは電話番号やメ payment.account.revolut.addUserNameInfo={0}\n現在の「Revolut」アカウント({1})には「ユーザ名」がありません。 \nアカウントデータを更新するのにRevolutの「ユーザ名」を入力して下さい。\nアカウント年齢署名状況に影響を及ぼしません。 payment.revolut.addUserNameInfo.headLine=Revolutアカウントをアップデートする +payment.cashapp.info=Cash App はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 +payment.venmo.info=Venmo はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 +payment.paypal.info=PayPal はほとんどの銀行振込よりもチャージバックリスクが高いことにご注意ください。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account @@ -2000,11 +2034,14 @@ payment.f2f.city=「対面」で会うための市区町村 payment.f2f.city.prompt=オファーとともに市区町村が表示されます payment.shared.optionalExtra=オプションの追加情報 payment.shared.extraInfo=追加情報 -payment.shared.extraInfo.prompt=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 +payment.shared.extraInfo.offer=追加のオファー情報 +payment.shared.extraInfo.prompt.paymentAccount=この支払いアカウントのオファーと一緒に表示したい特別な契約条件または詳細を定義して下さい(オファーを受ける前に、ユーザはこの情報を見れます)。 +payment.shared.extraInfo.prompt.offer=提供内容と共に表示したい特別な用語、条件、または詳細を定義してください。 +payment.shared.extraInfo.noDeposit=連絡先詳細およびオファー条件 payment.f2f.info=「対面」トレードには違うルールがあり、オンライントレードとは異なるリスクを伴います。\n\n主な違いは以下の通りです。\n●取引者は、提供される連絡先の詳細を使用して、出会う場所と時間に関する情報を交換する必要があります。\n●取引者は自分のノートパソコンを持ってきて、集合場所で「送金」と「入金」の確認をする必要があります。\n●メイカーに特別な「取引条件」がある場合は、アカウントの「追加情報」テキストフィールドにその旨を記載する必要があります。\n●オファーを受けると、テイカーはメイカーの「トレード条件」に同意したものとします。\n●係争が発生した場合、集合場所で何が起きたのかについての改ざん防止証明を入手することは通常困難であるため、調停者や調停人はあまりサポートをできません。このような場合、XMRの資金は無期限に、または取引者が合意に達するまでロックされる可能性があります。\n\n「対面」トレードでの違いを完全に理解しているか確認するためには、次のURLにある手順と推奨事項をお読みください:[HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Webページを開く payment.f2f.offerbook.tooltip.countryAndCity=国と都市: {0} / {1} -payment.f2f.offerbook.tooltip.extra=追加情報: {0} +payment.shared.extraInfo.tooltip=追加情報: {0} payment.japan.bank=銀行 payment.japan.branch=支店 @@ -2013,7 +2050,10 @@ payment.japan.recipient=名義 payment.australia.payid=PayID payment.payid=金融機関と繋がっているPayID。例えばEメールアドレスそれとも携帯電話番号。 payment.payid.info=銀行、信用金庫、あるいは住宅金融組合アカウントと安全に繋がれるPayIDとして使われる電話番号、Eメールアドレス、それともオーストラリア企業番号(ABN)。すでにオーストラリアの金融機関とPayIDを作った必要があります。送金と受取の金融機関は両方PayIDをサポートする必要があります。詳しくは以下を訪れて下さい [HYPERLINK:https://payid.com.au/faqs/] -payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 +payment.amazonGiftCard.info=アマゾンeGiftカードで支払うには、アマゾンアカウントを使ってeGiftカードをXMR売り手に送る必要があります。\n\nHavenoはeGiftカードの送り先になるXMR売り手のメールアドレスそれとも電話番号を表示します。そしてeGiftカードのメッセージフィールドに、必ずトレードIDを入力して下さい。最良の慣行について詳しくはWikiを参照して下さい:[HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card]\n\n3つの注意点:\n- 可能であれば、100米ドル価格以下のeGiftカードを送って下さい。それ以上の価格はアマゾンに不正な取引というフラグが立てられることがあります。\n- eGiftカードのメッセージフィールドに、トレードIDと一緒に信ぴょう性のあるメッセージを入力して下さい。(例えば隆さん、「お誕生日おめでとう!」)。(そして確認のため、取引者チャットでトレードピアにメッセージの内容を伝えて下さい)。\n- アマゾンeGiftカードは買われたサイトのみに交換できます(例えば、amazon.jpから買われたカードはamazon.jpのみに交換できます)。 +payment.paysafe.info=あなたの保護のため、支払いにPaysafecard PINの使用は強くお勧めしません。\n\n\ + PINを使用した取引は、紛争解決のために独立して確認することができません。問題が発生した場合、資金の回収が不可能になることがあります。\n\n\ + 取引の安全性と紛争解決を確保するため、常に確認可能な記録を提供する支払い方法を使用してください。 # We use constants from the code so we do not use our normal naming convention @@ -2093,7 +2133,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=アマゾンeGiftカード # suppress inspection "UnusedProperty" @@ -2145,7 +2185,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=アマゾンeGiftカード # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index b521753fea..03d90cc9fd 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação -shared.fundFromSavingsWalletButton=Transferir fundos da carteira Haveno +shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para prover fundos shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% abaixo do preço de mercado @@ -123,6 +125,7 @@ shared.noDateAvailable=Sem data disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar esse valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência @@ -139,6 +142,7 @@ shared.addNewAccount=Adicionar conta nova shared.ExportAccounts=Exportar Contas shared.importAccounts=Importar Contas shared.createNewAccount=Criar nova conta +shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta. shared.saveNewAccount=Salvar nova conta shared.selectedAccount=Conta selecionada shared.deleteAccount=Apagar conta @@ -185,13 +189,14 @@ shared.total=Total shared.totalsNeeded=Fundos necessária shared.tradeWalletAddress=Endereço da carteira de negociação shared.tradeWalletBalance=Saldo da carteira de negociação -shared.reserveExactAmount=Reserve apenas os fundos necessários. Pode ser necessário uma taxa de mineração e 10 confirmações (~20 minutos) antes que sua oferta esteja ativa. +shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e cerca de 20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo shared.openURL=Aberto {0} shared.fiat=Fiat shared.crypto=Cripto +shared.preciousMetals=Metais Preciosos shared.all=Todos shared.edit=Editar shared.advancedOptions=Opções avançadas @@ -331,6 +336,7 @@ offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Comprar {0} offerbook.takeOfferToSell=Vender {0} +offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Trader offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} @@ -340,7 +346,9 @@ offerbook.offerersAcceptedBankSeats=Países aceitos como sede bancária (tomador offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Ofertas que correspondem às minhas contas +offerbook.filterNoDeposit=Sem depósito +offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada por um árbitro e pode assinar contas de pares @@ -351,6 +359,8 @@ offerbook.timeSinceSigning.info.banned=conta foi banida offerbook.timeSinceSigning.daysSinceSigning={0} dias offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.buyXmrWith=Compre XMR com: +offerbook.sellXmrFor=Venda XMR por: offerbook.timeSinceSigning.help=Quando você completa uma negociação bem sucedida com um par que tem uma conta de pagamento assinada, a sua conta de pagamento é assinada.\n{0} dias depois, o limite inicial de {1} é levantado e sua conta pode assinar as contas de pagamento de outros pares. offerbook.timeSinceSigning.notSigned=Ainda não assinada @@ -365,6 +375,7 @@ offerbook.nrOffers=N.º de ofertas: {0} offerbook.volume={0} (mín. - máx.) 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.createNewOffer=Criar oferta para {0} {1} offerbook.createOfferToBuy=Criar oferta para comprar {0} offerbook.createOfferToSell=Criar oferta para vender {0} @@ -455,7 +466,12 @@ createOffer.placeOfferButton=Revisar: Criar oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia da negociação: {0} \n -createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\nEsses fundos ficam reservados na sua carteira local e ficarão travados no endereço de depósito multisig quando alguém aceitar a sua oferta.\n\nA quantia equivale à soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n- Taxa de mineração: {4}\n\nVocê pode financiar sua negociação das seguintes maneiras:\n- Usando a sua carteira Haveno (conveniente, mas transações poderão ser associadas entre si) OU\n- Usando uma carteira externa (maior privacidade)\n\nVocê verá todas as opções de financiamento e detalhes após fechar esta janela. +createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ + Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ + O valor é a soma de:\n\ + {1}\ + - Seu depósito de segurança: {2}\n\ + - Taxa de negociação: {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=Um erro ocorreu ao emitir uma oferta:\n\n{0}\n\nNenhum fundo foi retirado de sua carteira até agora.\nPor favor, reinicie o programa e verifique sua conexão de internet. @@ -477,7 +493,10 @@ createOffer.setDepositAsBuyer=Definir o meu depósito de segurança como comprad createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O seu depósito de segurança do comprador será de {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança como comprador será de {0} -createOffer.minSecurityDepositUsed=Depósito de segurança mínimo para compradores foi usado +createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado +createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito necessário do comprador (protegido por senha) +createOffer.myDeposit=Meu depósito de segurança (%) +createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### @@ -501,6 +520,8 @@ takeOffer.fundsBox.networkFee=Total em taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negociação Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de transação, {2} taxa de mineração) +takeOffer.fundsBox.noFundingRequiredTitle=Sem financiamento necessário +takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a frase secreta da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o status de sua negociação em \"Portfolio/Negociações em aberto\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta.\n\n{0} @@ -511,7 +532,7 @@ takeOffer.noPriceFeedAvailable=Você não pode aceitar essa oferta pois ela usa takeOffer.takeOfferFundWalletInfo.headline=Financiar sua negociação # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia a negociar: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nA quantia equivale a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n\nVocê pode escolher entre duas opções para financiar sua negociação:\n- Usar a sua carteira Haveno (conveniente, mas transações podem ser associadas entre si) OU\n- Transferir a partir de uma carteira externa (potencialmente mais privado)\n\nVocê verá todas as opções de financiamento e detalhes após fechar esta janela. +takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou por essa oferta, você pode retirar seus fundos na seção \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia @@ -597,7 +618,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada\n{0} precisa esperar ao menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação do depósito ainda não foi confirmada.\nIsto pode ocorrer em casos raros em que a taxa de financiamento de um dos negociadores enviada a partir de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode aguardar um pouco mais ou entrar em contato com o mediador para pedir assistência. @@ -622,7 +644,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANTE:\nApós ter feito o # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Envie {0} através de \"US Postal Money Order\" para o vendedor de XMR.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -800,11 +822,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1034,6 +1056,8 @@ settings.net.p2pHeader=Rede Haveno settings.net.onionAddressLabel=Meu endereço onion settings.net.xmrNodesLabel=Usar nodos personalizados do Monero settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexão +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor na rede Monero settings.net.moneroNodesLabel=Conexão a nodos do Monero settings.net.useProvidedNodesRadio=Usar nodos do Monero Core fornecidos @@ -1455,6 +1479,7 @@ offerDetailsWindow.confirm.maker=Criar oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Criada em offerDetailsWindow.makersOnion=Endereço onion do ofertante +offerDetailsWindow.challenge=Passphrase da oferta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1495,7 +1520,7 @@ tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução de disputas tradeDetailsWindow.headline=Negociação -tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado: +tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data da negociação tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação @@ -1569,7 +1594,7 @@ popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir arquivo de log popup.reportError.gitHub=Reportar à lista de problemas no GitHub -popup.reportError={0}\n\nPara nos ajudar a melhorar o aplicativo, reporte o bug criando um relatório (Issue) em nossa página do GitHub em https://github.com/bisq-network/bisq/issues.\n\nA mensagem de erro exibida acima será copiada para a área de transferência quando você clicar qualquer um dos botões abaixo.\nA solução de problemas será mais fácil se você anexar o arquivo haveno.log ao clicar em "Abrir arquivo de log", salvando uma cópia e incluindo-a em seu relatório do problema (Issue) no GitHub. +popup.reportError={0}\n\nPara nos ajudar a melhorar o aplicativo, reporte o bug criando um relatório (Issue) em nossa página do GitHub em https://github.com/haveno-dex/haveno/issues.\n\nA mensagem de erro exibida acima será copiada para a área de transferência quando você clicar qualquer um dos botões abaixo.\nA solução de problemas será mais fácil se você anexar o arquivo haveno.log ao clicar em "Abrir arquivo de log", salvando uma cópia e incluindo-a em seu relatório do problema (Issue) no GitHub. popup.error.tryRestart=Por favor, reinicie o aplicativo e verifique sua conexão de Internet para ver se o problema foi resolvido. popup.error.takeOfferRequestFailed=Houve um quando alguém tentou aceitar uma de suas ofertas:\n{0} @@ -1614,6 +1639,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Faça o update para a última versão do Haveno. Um update obrigatório foi lançado e desabilita negociações em versões antigas. Por favor, veja o Fórum do Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós seed. Por favor, informe aos administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederiam o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação de taxa de ofertante para a oferta com ID {0} foi rejeitada pela rede Monero.\nID da transação: {1}.\nA oferta foi removida para evitar problemas adicionais.\nPor favor, vá até "Configurações/Informações da rede" e ressincronize o arquivo SPV.\nPara mais informações, por favor acesse o canal #support do time da Haveno na Keybase. @@ -1681,6 +1707,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador +popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro de negociação fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. + #################################################################### # Notifications #################################################################### @@ -1806,11 +1835,14 @@ navigation.support=\"Suporte\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante: {1} de {0} / Aceitador: {3} de {2} +formatter.makerTaker.locked=Ofertante: {1} de {0} / Aceitador: {3} de {2} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você está {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador @@ -1953,17 +1985,15 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=Identificação pessoal -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Certifique-se de que o seu banco permite a realização de depósitos em espécie na conta de terceiros. @@ -1971,12 +2001,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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. +payment.venmo.info=Por favor, esteja ciente de que o Venmo tem um risco maior de estorno do que a maioria das transferências bancárias. +payment.paypal.info=Por favor, esteja ciente de que o PayPal tem um risco maior de estorno do que a maioria das transferências bancárias. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=Informações para contato payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informações para contato @@ -1985,11 +2017,13 @@ payment.f2f.city=Cidade para se encontrar 'Cara-a-cara' payment.f2f.city.prompt=A cidade será exibida na oferta payment.shared.optionalExtra=Informações adicionais opcionais payment.shared.extraInfo=Informações adicionais -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=Informações adicionais sobre a oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos, condições ou detalhes especiais que você gostaria que fossem exibidos com suas ofertas para esta conta de pagamento (os usuários verão estas informações antes de aceitar as ofertas). +payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. +payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info.openURL=Abrir site payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informações adicionais: {0} +payment.shared.extraInfo.tooltip=Informações adicionais: {0} payment.japan.bank=Banco payment.japan.branch=Ramo @@ -1998,7 +2032,10 @@ 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://haveno.exchange/wiki/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. # We use constants from the code so we do not use our normal naming convention @@ -2078,7 +2115,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2130,7 +2167,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 3567f188eb..0ec7c93184 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -40,6 +40,8 @@ shared.buyMonero=Comprar monero shared.sellMonero=Vender monero shared.buyCurrency=Comprar {0} shared.sellCurrency=Vender {0} +shared.buyCurrency.locked=Comprar {0} 🔒 +shared.sellCurrency.locked=Vender {0} 🔒 shared.buyingXMRWith=comprando XMR com {0} shared.sellingXMRFor=vendendo XMR por {0} shared.buyingCurrency=comprando {0} (vendendo XMR) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Sim, cancelar shared.nextStep=Próximo passo shared.selectTradingAccount=Selecionar conta de negociação -shared.fundFromSavingsWalletButton=Transferir fundos da carteira Haveno +shared.fundFromSavingsWalletButton=Aplicar fundos da carteira Haveno shared.fundFromExternalWalletButton=Abrir sua carteira externa para o financiamento shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Abaixo % do preço de mercado @@ -123,6 +125,7 @@ shared.noDateAvailable=Sem dada disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar este valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência @@ -139,6 +142,7 @@ shared.addNewAccount=Adicionar uma nova conta shared.ExportAccounts=Exportar Contas shared.importAccounts=Importar Contas shared.createNewAccount=Criar nova conta +shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta. shared.saveNewAccount=Guardar nova conta shared.selectedAccount=Conta selecionada shared.deleteAccount=Apagar conta @@ -185,13 +189,14 @@ shared.total=Total shared.totalsNeeded=Fundos necessários shared.tradeWalletAddress=Endereço da carteira do negócio shared.tradeWalletBalance=Saldo da carteira de negócio -shared.reserveExactAmount=Reserve apenas os fundos necessários. Pode ser necessário uma taxa de mineração e 10 confirmações (~20 minutos) antes que sua oferta seja ativada. +shared.reserveExactAmount=Reserve apenas os fundos necessários. Requer uma taxa de mineração e ~20 minutos antes que sua oferta seja publicada. shared.makerTxFee=Ofertante: {0} shared.takerTxFee=Aceitador: {0} shared.iConfirm=Eu confirmo shared.openURL=Abrir {0} shared.fiat=Moeda fiduciária shared.crypto=Cripto +shared.preciousMetals=Metais Preciosos shared.all=Tudo shared.edit=Editar shared.advancedOptions=Opções avançadas @@ -328,6 +333,7 @@ offerbook.createOffer=Criar oferta offerbook.takeOffer=Aceitar oferta offerbook.takeOfferToBuy=Aceitar oferta para comprar {0} offerbook.takeOfferToSell=Aceitar oferta para vender {0} +offerbook.takeOffer.enterChallenge=Digite a senha da oferta offerbook.trader=Negociador offerbook.offerersBankId=ID do banco do ofertante (BIC/SWIFT): {0} offerbook.offerersBankName=Nome do banco do ofertante: {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=Sede do banco aceite (aceitador):\n {0} offerbook.availableOffers=Ofertas disponíveis offerbook.filterByCurrency=Filtrar por moeda offerbook.filterByPaymentMethod=Filtrar por método de pagamento -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Ofertas que correspondem às minhas contas +offerbook.filterNoDeposit=Sem depósito +offerbook.noDepositOffers=Ofertas sem depósito (senha necessária) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=Esta conta foi verificada e {0} offerbook.timeSinceSigning.info.arbitrator=assinada pelo árbitro e pode assinar contas de pares @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} dias offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.buyXmrWith=Compre XMR com: +offerbook.sellXmrFor=Venda XMR por: offerbook.timeSinceSigning.help=Quando você completa com sucesso um negócio com um par que tenha uma conta de pagamento assinada, a sua conta de pagamento é assinada .\n{0} dias depois, o limite inicial de {1} é aumentado e a sua conta pode assinar contas de pagamento de outros pares. offerbook.timeSinceSigning.notSigned=Ainda não assinada @@ -362,6 +372,7 @@ offerbook.nrOffers=Nº de ofertas: {0} offerbook.volume={0} (mín - máx) 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.createNewOffer=Criar oferta para {0} {1} offerbook.createOfferToBuy=Criar nova oferta para comprar {0} offerbook.createOfferToSell=Criar nova oferta para vender {0} @@ -452,7 +463,12 @@ createOffer.placeOfferButton=Rever: Colocar oferta para {0} monero createOffer.createOfferFundWalletInfo.headline=Financiar sua oferta # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n -createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\nEsses fundos estão reservados na sua carteira local e serão bloqueados no endereço de depósito multi-assinatura assim que alguém aceitar a sua oferta.\n\nA quantia é a soma de:\n{1} - Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n- Taxa de mineração: {4}\n\nVocê pode escolher entre duas opções ao financiar o seu negócio:\n- Use sua carteira Haveno (conveniente, mas as transações podem ser conectadas) OU\n- Transferência de uma carteira externa (potencialmente mais privada)\n\nVocê verá todas as opções de financiamento e detalhes depois de fechar este popup. +createOffer.createOfferFundWalletInfo.msg=Você precisa depositar {0} para esta oferta.\n\n\ + Esses fundos são reservados em sua carteira local e serão bloqueados em uma carteira multisig assim que alguém aceitar sua oferta.\n\n\ + O valor é a soma de:\n\ + {1}\ + - Seu depósito de segurança: {2}\n\ + - Taxa de negociação: {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=Ocorreu um erro ao colocar a oferta:\n\n{0}\n\nAinda nenhuns fundos saíram da sua carteira.\nPor favor, reinicie seu programa e verifique sua conexão de rede. @@ -474,7 +490,10 @@ createOffer.setDepositAsBuyer=Definir o meu depósito de segurança enquanto com createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=O depósito de segurança do seu comprador será {0} createOffer.securityDepositInfoAsBuyer=O seu depósito de segurança enquanto comprador será {0} -createOffer.minSecurityDepositUsed=O mín. depósito de segurança para o comprador é utilizado +createOffer.minSecurityDepositUsed=O depósito de segurança mínimo é utilizado +createOffer.buyerAsTakerWithoutDeposit=Nenhum depósito exigido do comprador (protegido por frase secreta) +createOffer.myDeposit=Meu depósito de segurança (%) +createOffer.myDepositInfo=Seu depósito de segurança será {0} #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=Total de taxas de mineração takeOffer.fundsBox.takeOfferSpinnerInfo=Aceitando a oferta: {0} takeOffer.fundsBox.paymentLabel=negócio do Haveno com ID {0} takeOffer.fundsBox.fundsStructure=({0} depósito de segurança, {1} taxa de negócio, {2} taxa de mineração) +takeOffer.fundsBox.noFundingRequiredTitle=Nenhum financiamento necessário +takeOffer.fundsBox.noFundingRequiredDescription=Obtenha a senha da oferta com o vendedor fora do Haveno para aceitar esta oferta. takeOffer.success.headline=Você aceitou uma oferta com sucesso. takeOffer.success.info=Você pode ver o estado de seu negócio em \"Portefólio/Negócios abertos\". takeOffer.error.message=Ocorreu um erro ao aceitar a oferta .\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=Você não pode aceitar aquela oferta pois ela ut takeOffer.takeOfferFundWalletInfo.headline=Financiar seu negócio # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Quantia de negócio: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nA quantia é a soma de:\n{1} - Seu depósito de segurança: {2}\n- Taxa de negociação: {3}\n\nVocê pode escolher entre duas opções ao financiar o seu negócio:\n- Use sua carteira Haveno (conveniente, mas as transações podem ser conectas) OU\n- Transferência de uma carteira externa (potencialmente mais privada)\n\nVocê verá todas as opções de financiamento e detalhes depois de fechar este popup. +takeOffer.takeOfferFundWalletInfo.msg=Você precisa depositar {0} para aceitar esta oferta.\n\nO valor é a soma de:\n{1}- Seu depósito de segurança: {2}\n- Taxa de negociação: {3} takeOffer.alreadyPaidInFunds=Se você já pagou com seus fundos você pode levantá-los na janela \"Fundos/Enviar fundos\". takeOffer.paymentInfo=Informações de pagamento takeOffer.setAmountPrice=Definir quantia @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada.\n{0} precisa aguardar pelo menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação de depósito ainda não foi confirmada. Isso pode acontecer em casos raros, quando a taxa de financiamento de um negociador proveniente de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode esperar mais tempo ou entrar em contato com o mediador para obter assistência. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=REQUISITO IMPORTANTE:\nDepois d # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Por favor envie {0} por \"US Postal Money Order\" para o vendedor de XMR.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1031,6 +1053,8 @@ settings.net.p2pHeader=Rede do Haveno settings.net.onionAddressLabel=O meu endereço onion settings.net.xmrNodesLabel=Usar nós de Monero personalizados settings.net.moneroPeersLabel=Pares conectados +settings.net.connection=Conexão +settings.net.connected=Conectado settings.net.useTorForXmrJLabel=Usar Tor para a rede de Monero settings.net.moneroNodesLabel=Nós de Monero para conectar settings.net.useProvidedNodesRadio=Usar nós de Monero Core providenciados @@ -1448,6 +1472,7 @@ offerDetailsWindow.confirm.maker=Confirmar: Criar oferta para {0} monero offerDetailsWindow.confirm.taker=Confirmar: Aceitar oferta de {0} monero offerDetailsWindow.creationDate=Data de criação offerDetailsWindow.makersOnion=Endereço onion do ofertante +offerDetailsWindow.challenge=Passphrase da oferta qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1488,7 +1513,7 @@ tacWindow.disagree=Eu não concordo e desisto tacWindow.arbitrationSystem=Resolução da disputa tradeDetailsWindow.headline=Negócio -tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado: +tradeDetailsWindow.disputedPayoutTxId=ID de transação do pagamento disputado tradeDetailsWindow.tradeDate=Data de negócio tradeDetailsWindow.txFee=Taxa de mineração tradeDetailsWindow.tradePeersOnion=Endereço onion dos parceiros de negociação @@ -1561,7 +1586,7 @@ popup.headline.error=Erro popup.doNotShowAgain=Não mostrar novamente popup.reportError.log=Abrir ficheiro de log popup.reportError.gitHub=Relatar ao GitHub issue tracker -popup.reportError={0}\n\nPara nos ajudar a melhorar o software, por favor reporte este erro abrindo um novo issue em https://github.com/bisq-network/bisq/issues.\nA mensagem de erro acima será copiada para a área de transferência quando você clicar num dos botões abaixo.\nSerá mais fácil fazer a depuração se você incluir o ficheiro haveno.log clicando "Abrir arquivo de log", salvando uma cópia e anexando-a ao seu relatório de erros. +popup.reportError={0}\n\nPara nos ajudar a melhorar o software, por favor reporte este erro abrindo um novo issue em https://github.com/haveno-dex/haveno/issues.\nA mensagem de erro acima será copiada para a área de transferência quando você clicar num dos botões abaixo.\nSerá mais fácil fazer a depuração se você incluir o ficheiro haveno.log clicando "Abrir arquivo de log", salvando uma cópia e anexando-a ao seu relatório de erros. popup.error.tryRestart=Por favor tente reiniciar o programa e verifique a sua conexão de Internet para ver se pode resolver o problema. popup.error.takeOfferRequestFailed=Ocorreu um erro quando alguém tentou aceitar uma das suas ofertas:\n{0} @@ -1604,6 +1629,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=transmissão de preço popup.warning.seed=semente popup.warning.mandatoryUpdate.trading=Por favor, atualize para a versão mais recente do Haveno. Uma atualização obrigatória que desativa negociação para versões antigas foi lançada. Por favor, confira o Fórum Haveno para mais informações. +popup.warning.noFilter=Não recebemos um objeto de filtro dos nós sementes. Por favor, informe os administradores da rede para registrar um objeto de filtro. popup.warning.burnXMR=Esta transação não é possível, pois as taxas de mineração de {0} excederia o montante a transferir de {1}. Aguarde até que as taxas de mineração estejam novamente baixas ou até você ter acumulado mais XMR para transferir. popup.warning.openOffer.makerFeeTxRejected=A transação da taxa de ofertante para a oferta com o ID {0} foi rejeitada pela rede do Monero.\nID da transação={1}.\nA oferta foi removida para evitar futuros problemas.\nPor favor vá à \"Definições/Informação da Rede\" e re-sincronize o ficheiro SPV.\nPara mais ajuda por favor contacte o canal de apoio do Haveno na equipa Keybase do Haveno. @@ -1671,6 +1697,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Nenhum depósito exigido do comprador +popup.info.buyerAsTakerWithoutDeposit=Sua oferta não exigirá um depósito de segurança ou taxa do comprador de XMR.\n\nPara aceitar sua oferta, você deve compartilhar uma senha com seu parceiro comercial fora do Haveno.\n\nA senha é gerada automaticamente e exibida nos detalhes da oferta após a criação. + #################################################################### # Notifications #################################################################### @@ -1796,11 +1825,14 @@ navigation.support=\"Apoio\" formatter.formatVolumeLabel={0} quantia{1} formatter.makerTaker=Ofertante como {0} {1} / Aceitador como {2} {3} +formatter.makerTaker.locked=Ofertante como {0} {1} / Aceitador como {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Você é {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Você está criando uma oferta para {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Você está criando uma oferta para {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Você está criando uma oferta para {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Você está criando uma oferta para {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} como ofertante formatter.asTaker={0} {1} como aceitador @@ -1943,17 +1975,15 @@ payment.accountType=Tipo de conta payment.checking=Conta Corrente payment.savings=Poupança payment.personalId=ID pessoal -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. @@ -1961,12 +1991,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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. +payment.venmo.info=Esteja ciente de que o Venmo tem um risco de estorno maior do que a maioria das transferências bancárias. +payment.paypal.info=Esteja ciente de que o PayPal tem um risco de estorno maior do que a maioria das transferências bancárias. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=Informação de contacto payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Informação de contacto @@ -1975,11 +2007,13 @@ payment.f2f.city=Cidade para o encontro 'Face à face' payment.f2f.city.prompt=A cidade será exibida com a oferta payment.shared.optionalExtra=Informação adicional opcional payment.shared.extraInfo=Informação adicional -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=Informações adicionais sobre a oferta +payment.shared.extraInfo.prompt.paymentAccount=Defina quaisquer termos especiais, condições ou detalhes que você gostaria de exibir com suas ofertas para esta conta de pagamento (os usuários verão essas informações antes de aceitar as ofertas). +payment.shared.extraInfo.prompt.offer=Defina quaisquer termos, condições ou detalhes especiais que você gostaria de exibir com sua oferta. +payment.shared.extraInfo.noDeposit=Detalhes de contato e termos da oferta payment.f2f.info.openURL=Abrir página web payment.f2f.offerbook.tooltip.countryAndCity=País e cidade: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Informação adicional: {0} +payment.shared.extraInfo.tooltip=Informação adicional: {0} payment.japan.bank=Banco payment.japan.branch=Agência @@ -1988,7 +2022,10 @@ 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://haveno.exchange/wiki/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. # We use constants from the code so we do not use our normal naming convention @@ -2068,7 +2105,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2120,7 +2157,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 9e67e9e75b..46828f97b1 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -40,6 +40,8 @@ shared.buyMonero=Купить биткойн shared.sellMonero=Продать биткойн shared.buyCurrency=Купить {0} shared.sellCurrency=Продать {0} +shared.buyCurrency.locked=Купить {0} 🔒 +shared.sellCurrency.locked=Продать {0} 🔒 shared.buyingXMRWith=покупка ВТС за {0} shared.sellingXMRFor=продажа ВТС за {0} shared.buyingCurrency=покупка {0} (продажа ВТС) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Да, отменить shared.nextStep=Далее shared.selectTradingAccount=Выбрать торговый счёт -shared.fundFromSavingsWalletButton=Перевести средства с кошелька Haveno +shared.fundFromSavingsWalletButton=Применить средства из кошелька Haveno shared.fundFromExternalWalletButton=Открыть внешний кошелёк для пополнения shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=% ниже рыночного курса @@ -123,6 +125,7 @@ shared.noDateAvailable=Дата не указана shared.noDetailsAvailable=Подробности не указаны shared.notUsedYet=Ещё не использовано shared.date=Дата +shared.sendFundsDetailsWithFee=Отправка: {0}\n\nНа получающий адрес: {1}\n\nДополнительная комиссия майнера: {2}\n\nВы уверены, что хотите отправить эту сумму? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Скопировать в буфер @@ -139,6 +142,7 @@ shared.addNewAccount=Добавить новый счёт shared.ExportAccounts=Экспортировать счета shared.importAccounts=Импортировать счета shared.createNewAccount=Создать новый счёт +shared.createNewAccountDescription=Данные вашей учетной записи хранятся локально на вашем устройстве и передаются только вашему торговому партнеру и арбитру, если открывается спор. shared.saveNewAccount=Сохранить новый счёт shared.selectedAccount=Выбранный счёт shared.deleteAccount=Удалить счёт @@ -185,13 +189,14 @@ shared.total=Всего shared.totalsNeeded=Требуемая сумма shared.tradeWalletAddress=Адрес кошелька сделки shared.tradeWalletBalance=Баланс кошелька сделки -shared.reserveExactAmount=Зарезервируйте только необходимые средства. Может потребоваться комиссия за майнинг и 10 подтверждений (~20 минут), прежде чем ваше предложение станет активным. +shared.reserveExactAmount=Резервируйте только необходимые средства. Требуется комиссия за майнинг и ~20 минут, прежде чем ваше предложение станет активным. shared.makerTxFee=Мейкер: {0} shared.takerTxFee=Тейкер: {0} shared.iConfirm=Подтверждаю shared.openURL=Открыть {0} shared.fiat=Нац. валюта shared.crypto=Криптовалюта +shared.preciousMetals=Драгоценные металлы shared.all=Все shared.edit=Редактировать shared.advancedOptions=Дополнительные настройки @@ -328,6 +333,7 @@ offerbook.createOffer=Создать предложение offerbook.takeOffer=Принять предложение offerbook.takeOfferToBuy=Принять предложение купить {0} offerbook.takeOfferToSell=Принять предложение продать {0} +offerbook.takeOffer.enterChallenge=Введите фразу-пароль предложения offerbook.trader=Трейдер offerbook.offerersBankId=Идент. банка (BIC/SWIFT) мейкера: {0} offerbook.offerersBankName=Название банка мейкера: {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=Допустимые страны банка offerbook.availableOffers=Доступные предложения offerbook.filterByCurrency=Фильтровать по валюте offerbook.filterByPaymentMethod=Фильтровать по способу оплаты -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Предложения, соответствующие моим аккаунтам +offerbook.filterNoDeposit=Нет депозита +offerbook.noDepositOffers=Предложения без депозита (требуется пароль) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} дн. offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing 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.notSigned=Not signed yet @@ -362,6 +372,7 @@ offerbook.nrOffers=Кол-во предложений: {0} offerbook.volume={0} (мин. ⁠— макс.) 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.createNewOffer=Создать предложение для {0} {1} offerbook.createOfferToBuy=Создать новое предложение на покупку {0} offerbook.createOfferToSell=Создать новое предложение на продажу {0} @@ -452,7 +463,12 @@ createOffer.placeOfferButton=Проверка: разместить предло createOffer.createOfferFundWalletInfo.headline=Обеспечить своё предложение # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n -createOffer.createOfferFundWalletInfo.msg=Вы должны внести {0} для обеспечения этого предложения.\n\nЭти средства будут зарезервированы в вашем локальном кошельке, а когда кто-то примет ваше предложение — заблокированы на депозитном multisig-адресе.\n\nСумма состоит из:\n{1}- вашего залога: {2},\n- комиссии за сделку: {3},\n- комиссии майнера: {4}.\n\nВы можете выбрать один из двух вариантов финансирования сделки:\n - использовать свой кошелёк Haveno (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. +createOffer.createOfferFundWalletInfo.msg=Вам нужно внести депозит {0} для этого предложения.\n\n\ + Эти средства резервируются в вашем локальном кошельке и будут заблокированы в мультиподписном кошельке, как только кто-то примет ваше предложение.\n\n\ + Сумма состоит из:\n\ + {1}\ + - Ваш залог: {2}\n\ + - Торговая комиссия: {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=Ошибка при создании предложения:\n\n{0}\n\nВаши средства остались в кошельке.\nПерезагрузите приложение и проверьте сетевое соединение. @@ -474,7 +490,10 @@ createOffer.setDepositAsBuyer=Установить мой залог как по createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Сумма залога покупателя: {0} createOffer.securityDepositInfoAsBuyer=Сумма вашего залога: {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Минимальный залог используется +createOffer.buyerAsTakerWithoutDeposit=Залог от покупателя не требуется (защищено паролем) +createOffer.myDeposit=Мой залог (%) +createOffer.myDepositInfo=Ваш залог составит {0} #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=Oбщая комиссия майнера takeOffer.fundsBox.takeOfferSpinnerInfo=Принятие предложения: {0} takeOffer.fundsBox.paymentLabel=Сделка в Haveno с идентификатором {0} takeOffer.fundsBox.fundsStructure=({0} — залог, {1} — комиссия за сделку, {2} — комиссия майнера) +takeOffer.fundsBox.noFundingRequiredTitle=Не требуется финансирование +takeOffer.fundsBox.noFundingRequiredDescription=Получите пароль предложения от продавца вне Haveno, чтобы принять это предложение. takeOffer.success.headline=Вы успешно приняли предложение. takeOffer.success.info=Статус вашей сделки отображается в разделе \«Папка/Текущие сделки\». takeOffer.error.message=Ошибка при принятии предложения:\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=Нельзя принять это предлож takeOffer.takeOfferFundWalletInfo.headline=Обеспечьте свою сделку # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Сумма сделки: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Вы должны внести {0} для принятия этого предложения.\n\nСумма состоит из:\n{1}- вашего залога: {2},\n- комиссии за сделку: {3}\n\nВы можете выбрать один из двух вариантов финансирования сделки:\n - использовать свой кошелёк Haveno (удобно, но сделки можно отследить) ИЛИ\n - перевести из внешнего кошелька (потенциально более анонимно).\n\nВы увидите все варианты обеспечения предложения и их подробности после закрытия этого окна. +takeOffer.takeOfferFundWalletInfo.msg=Вам нужно внести депозит в размере {0} для принятия этого предложения.\n\nСумма составляет:\n{1}- Ваш залог: {2}\n- Торговая комиссия: {3} takeOffer.alreadyPaidInFunds=Если вы уже внесли средства, их можно вывести в разделе \«Средства/Отправить средства\». takeOffer.paymentInfo=Информация о платеже takeOffer.setAmountPrice=Задайте сумму @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Депозитная транзакция опубликована.\n{0} должен дождаться хотя бы одного подтверждения в блокчейне перед началом платежа. +portfolio.pending.step1.info.you=Транзакция депозита была опубликована.\nВам нужно дождаться 10 подтверждений (около 20 минут), прежде чем платеж сможет начаться. +portfolio.pending.step1.info.buyer=Транзакция депозита была опубликована.\nПокупатель XMR должен подождать 10 подтверждений (около 20 минут), прежде чем платеж может быть начат. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=ВАЖНОЕ ТРЕБОВАНИ # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Отправьте {0} \«Почтовым денежным переводом США\» продавцу XMR.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1030,6 +1052,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Мой onion-адрес settings.net.xmrNodesLabel=Использовать особые узлы Monero settings.net.moneroPeersLabel=Подключенные пиры +settings.net.connection=Соединение +settings.net.connected=Подключено settings.net.useTorForXmrJLabel=Использовать Tor для сети Monero settings.net.moneroNodesLabel=Узлы Monero для подключения settings.net.useProvidedNodesRadio=Использовать предоставленные узлы Monero Core @@ -1449,6 +1473,7 @@ offerDetailsWindow.confirm.maker=Подтвердите: разместить п offerDetailsWindow.confirm.taker=Подтвердите: принять предложение {0} биткойн offerDetailsWindow.creationDate=Дата создания offerDetailsWindow.makersOnion=Onion-адрес мейкера +offerDetailsWindow.challenge=Пароль предложения qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1562,7 +1587,7 @@ popup.headline.error=Ошибка popup.doNotShowAgain=Не показывать снова popup.reportError.log=Открыть файл журнала popup.reportError.gitHub=Сообщить о проблеме в Github -popup.reportError={0}\n\nЧтобы помочь нам улучшить приложение, просьба сообщить об ошибке, открыв новую тему на https://github.com/bisq-network/bisq/issues. \nСообщение об ошибке будет скопировано в буфер обмена при нажатии любой из кнопок ниже.\nЕсли вы прикрепите к отчету о неисправности файл журнала haveno.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. +popup.reportError={0}\n\nЧтобы помочь нам улучшить приложение, просьба сообщить об ошибке, открыв новую тему на https://github.com/haveno-dex/haveno/issues. \nСообщение об ошибке будет скопировано в буфер обмена при нажатии любой из кнопок ниже.\nЕсли вы прикрепите к отчету о неисправности файл журнала haveno.log, нажав «Открыть файл журнала» и сохранив его копию, это поможет нам разобраться с проблемой быстрее. popup.error.tryRestart=Попробуйте перезагрузить приложение и проверьте подключение к сети, чтобы попробовать решить проблему. popup.error.takeOfferRequestFailed=Произошла ошибка, когда контрагент попытался принять одно из ваших предложений:\n{0} @@ -1605,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=ретранслятор курса popup.warning.seed=мнемоническая фраза popup.warning.mandatoryUpdate.trading=Обновите Haveno до последней версии. Вышло обязательное обновление, которое делает невозможной торговлю в старых версиях приложения. Посетите форум Haveno, чтобы узнать подробности. +popup.warning.noFilter=Мы не получили объект фильтра от узлов-источников. Пожалуйста, сообщите администраторам сети, чтобы они зарегистрировали объект фильтра. popup.warning.burnXMR=Данную транзакцию невозможно завершить, так как плата за нее ({0}) превышает сумму перевода ({1}). Подождите, пока плата за транзакцию не снизится или пока у вас не появится больше XMR для завершения перевода. 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. @@ -1672,6 +1698,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Депозит от покупателя не требуется +popup.info.buyerAsTakerWithoutDeposit=Ваше предложение не потребует залога или комиссии от покупателя XMR.\n\nЧтобы принять ваше предложение, вы должны поделиться парольной фразой с вашим торговым партнером вне Haveno.\n\nПарольная фраза генерируется автоматически и отображается в деталях предложения после его создания. + #################################################################### # Notifications #################################################################### @@ -1797,11 +1826,14 @@ navigation.support=\«Поддержка\» formatter.formatVolumeLabel={0} сумма {1} formatter.makerTaker=Мейкер как {0} {1} / Тейкер как {2} {3} +formatter.makerTaker.locked=Мейкер как {0} {1} / Тейкер как {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Вы {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Вы создаете предложение {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Вы создаете предложение {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Вы создаете предложение {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Вы создаете предложение {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} как мейкер formatter.asTaker={0} {1} как тейкер @@ -1944,17 +1976,15 @@ payment.accountType=Тип счёта payment.checking=Текущий payment.savings=Сберегательный payment.personalId=Личный идентификатор -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. @@ -1962,12 +1992,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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 имеет более высокий риск возврата платежей, чем большинство банковских переводов. +payment.venmo.info=Обратите внимание, что Venmo имеет более высокий риск возврата платежей, чем большинство банковских переводов. +payment.paypal.info=Обратите внимание, что PayPal имеет более высокий риск возврата платежей, чем большинство банковских переводов. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=Контактная информация payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=Контактная информация @@ -1976,11 +2008,14 @@ payment.f2f.city=Город для личной встречи payment.f2f.city.prompt=Город будет указан в предложении payment.shared.optionalExtra=Дополнительная необязательная информация payment.shared.extraInfo=Дополнительная информация -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=Дополнительная информация о предложении +payment.shared.extraInfo.prompt.paymentAccount=Определите любые специальные термины, условия или детали, которые вы хотите, чтобы отображались с вашими предложениями для этого платежного аккаунта (пользователи увидят эту информацию перед принятием предложений). +payment.shared.extraInfo.prompt.offer=Определите любые специальные условия, требования или детали, которые вы хотели бы указать в своем предложении. +payment.shared.extraInfo.noDeposit=Контактные данные и условия предложения payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] payment.f2f.info.openURL=Открыть веб-страницу payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Дополнительная информация: {0} +payment.shared.extraInfo.tooltip=Дополнительная информация: {0} payment.japan.bank=Банк payment.japan.branch=Branch @@ -1989,7 +2024,10 @@ 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://haveno.exchange/wiki/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\ + Чтобы обеспечить безопасность транзакций с возможностью разрешения споров, всегда используйте методы оплаты, предоставляющие проверяемые записи. # We use constants from the code so we do not use our normal naming convention @@ -2069,7 +2107,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2121,7 +2159,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 34f0b3dda7..56fe9e67c9 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -40,6 +40,8 @@ shared.buyMonero=ซื้อ monero (บิตคอยน์) shared.sellMonero=ขาย monero (บิตคอยน์) shared.buyCurrency=ซื้อ {0} shared.sellCurrency=ขาย {0} +shared.buyCurrency.locked=ซื้อ {0} 🔒 +shared.sellCurrency.locked=ขาย {0} 🔒 shared.buyingXMRWith=การซื้อ XMR กับ {0} shared.sellingXMRFor=การขาย XMR แก่ {0} shared.buyingCurrency=การซื้อ {0} (การขาย XMR) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=ใช่ ยกเลิก shared.nextStep=ขั้นถัดไป shared.selectTradingAccount=เลือกบัญชีการซื้อขาย -shared.fundFromSavingsWalletButton=โอนเงินจาก Haveno wallet +shared.fundFromSavingsWalletButton=ใช้เงินจากกระเป๋าเงิน Haveno shared.fundFromExternalWalletButton=เริ่มทำการระดมเงินทุนหาแหล่งเงินจากกระเป๋าสตางค์ภายนอกของคุณ shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=ต่ำกว่า % จากราคาตลาด @@ -123,6 +125,7 @@ shared.noDateAvailable=ไม่มีวันที่ให้แสดง shared.noDetailsAvailable=ไม่มีรายละเอียด shared.notUsedYet=ยังไม่ได้ใช้งาน shared.date=วันที่ +shared.sendFundsDetailsWithFee=กำลังส่ง: {0}\n\nไปยังที่อยู่ผู้รับ: {1}\n\nค่าธรรมเนียมเหมืองเพิ่มเติม: {2}\n\nคุณแน่ใจหรือไม่ว่าต้องการส่งจำนวนนี้? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=คัดลอกไปที่คลิปบอร์ด @@ -139,6 +142,7 @@ shared.addNewAccount=เพิ่มบัญชีใหม่ shared.ExportAccounts=บัญชีส่งออก shared.importAccounts=บัญชีนำเข้า shared.createNewAccount=สร้างบัญชีใหม่ +shared.createNewAccountDescription=รายละเอียดบัญชีของคุณถูกจัดเก็บไว้ในอุปกรณ์ของคุณและจะแบ่งปันเฉพาะกับคู่ค้าของคุณและผู้ตัดสินหากมีการเปิดข้อพิพาท shared.saveNewAccount=บันทึกบัญชีใหม่ shared.selectedAccount=บัญชีที่เลือก shared.deleteAccount=ลบบัญชี @@ -185,13 +189,14 @@ shared.total=ยอดทั้งหมด shared.totalsNeeded=เงินที่จำเป็น shared.tradeWalletAddress=ที่อยู่ Trade wallet shared.tradeWalletBalance=ยอดคงเหลือของ Trade wallet -shared.reserveExactAmount=สงวนเฉพาะเงินทุนที่จำเป็นเท่านั้น อาจจะต้องเสียค่าขุดแร่และรับรอง 10 ครั้ง (~20 นาที) ก่อนที่ข้อเสนอของคุณจะเป็นสถานะออนไลน์ +shared.reserveExactAmount=สำรองเฉพาะเงินที่จำเป็น ต้องใช้ค่าธรรมเนียมการขุดและเวลาประมาณ 20 นาทีก่อนที่ข้อเสนอของคุณจะเผยแพร่ shared.makerTxFee=ผู้ทำ: {0} shared.takerTxFee=ผู้รับ: {0} shared.iConfirm=ฉันยืนยัน shared.openURL=เปิด {0} shared.fiat=คำสั่ง shared.crypto=คริปโต +shared.preciousMetals=โลหะมีค่า shared.all=ทั้งหมด shared.edit=แก้ไข shared.advancedOptions=ทางเลือกขั้นสูง @@ -328,6 +333,7 @@ offerbook.createOffer=สร้างข้อเสนอ offerbook.takeOffer=รับข้อเสนอ offerbook.takeOfferToBuy=Take offer to buy {0} offerbook.takeOfferToSell=Take offer to sell {0} +offerbook.takeOffer.enterChallenge=กรอกพาสเฟรสข้อเสนอ offerbook.trader=Trader (เทรดเดอร์) offerbook.offerersBankId=รหัสธนาคารของผู้สร้าง (BIC / SWIFT): {0} offerbook.offerersBankName=ชื่อธนาคารของผู้สร้าง: {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=ยอมรับตำแหน่งป offerbook.availableOffers=ข้อเสนอที่พร้อมใช้งาน offerbook.filterByCurrency=กรองตามสกุลเงิน offerbook.filterByPaymentMethod=ตัวกรองตามวิธีการชำระเงิน -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=ข้อเสนอที่ตรงกับบัญชีของฉัน +offerbook.filterNoDeposit=ไม่มีเงินมัดจำ +offerbook.noDepositOffers=ข้อเสนอที่ไม่มีเงินมัดจำ (ต้องการรหัสผ่าน) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} วัน offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing 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.notSigned=Not signed yet @@ -362,6 +372,7 @@ offerbook.nrOffers=No. ของข้อเสนอ: {0} offerbook.volume={0} (ต่ำสุด - สูงสุด) 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.createNewOffer=สร้างข้อเสนอให้กับ {0} {1} offerbook.createOfferToBuy=Create new offer to buy {0} offerbook.createOfferToSell=Create new offer to sell {0} @@ -452,7 +463,12 @@ createOffer.placeOfferButton=รีวิว: ใส่ข้อเสนอไ createOffer.createOfferFundWalletInfo.headline=เงินทุนสำหรับข้อเสนอของคุณ # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} -createOffer.createOfferFundWalletInfo.msg=คุณต้องวางเงินมัดจำ {0} ข้อเสนอนี้\n\nเงินเหล่านั้นจะถูกสงวนไว้ใน wallet ภายในประเทศของคุณและจะถูกล็อคไว้ในที่อยู่ที่ฝากเงิน multisig เมื่อมีคนรับข้อเสนอของคุณ\n\nผลรวมของจำนวนของ: \n{1} - เงินประกันของคุณ: {2} \n- ค่าธรรมเนียมการซื้อขาย: {3} \n- ค่าขุด: {4} \n\nคุณสามารถเลือกระหว่างสองตัวเลือกเมื่อมีการระดุมทุนการซื้อขายของคุณ: \n- ใช้กระเป๋าสตางค์ Haveno ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากเงินภายนอกเข้ามา (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการระดมทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ +createOffer.createOfferFundWalletInfo.msg=คุณจำเป็นต้องฝากเงิน {0} เพื่อข้อเสนอนี้\n\n\ + เงินเหล่านี้จะถูกสงวนไว้ในกระเป๋าเงินในเครื่องของคุณ และจะถูกล็อกในกระเป๋าเงินมัลติซิกเมื่อมีคนรับข้อเสนอของคุณ\n\n\ + จำนวนเงินคือผลรวมของ:\n\ + {1}\ + - เงินประกันของคุณ: {2}\n\ + - ค่าธรรมเนียมการซื้อขาย: {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=เกิดข้อผิดพลาดขณะใส่ข้อเสนอ: \n\n{0} \n\nยังไม่มีการโอนเงินจาก wallet ของคุณเลย\nโปรดเริ่มแอปพลิเคชันใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณ @@ -474,7 +490,10 @@ 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.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=เงินประกันความปลอดภัยขั้นต่ำถูกใช้ +createOffer.buyerAsTakerWithoutDeposit=ไม่ต้องวางมัดจำจากผู้ซื้อ (ป้องกันด้วยรหัสผ่าน) +createOffer.myDeposit=เงินประกันความปลอดภัยของฉัน (%) +createOffer.myDepositInfo=เงินประกันความปลอดภัยของคุณจะเป็น {0} #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=ยอดรวมค่าธรรมเนี takeOffer.fundsBox.takeOfferSpinnerInfo=ยอมรับข้อเสนอ: {0} takeOffer.fundsBox.paymentLabel=การซื้อขาย Haveno ด้วย ID {0} takeOffer.fundsBox.fundsStructure=({0} เงินประกัน {1} ค่าธรรมเนียมการซื้อขาย {2} ค่าธรรมเนียมการขุด) +takeOffer.fundsBox.noFundingRequiredTitle=ไม่ต้องใช้เงินทุน +takeOffer.fundsBox.noFundingRequiredDescription=รับรหัสผ่านข้อเสนอจากผู้ขายภายนอก Haveno เพื่อรับข้อเสนอนี้ takeOffer.success.headline=คุณได้รับข้อเสนอเป็นที่เรีบยร้อยแล้ว takeOffer.success.info=คุณสามารถดูสถานะการค้าของคุณได้ที่ \ "Portfolio (แฟ้มผลงาน) / เปิดการซื้อขาย \" takeOffer.error.message=เกิดข้อผิดพลาดขณะรับข้อเสนอ\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=คุณไม่สามารถรับข takeOffer.takeOfferFundWalletInfo.headline=ทุนการซื้อขายของคุณ # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- ปริมาณการซื้อขาย: {0} -takeOffer.takeOfferFundWalletInfo.msg=คุณต้องวางเงินประกัน {0} เพื่อรับข้อเสนอนี้\n\nจำนวนเงินคือผลรวมของ: \n{1} - เงินประกันของคุณ: {2} \n- ค่าธรรมเนียมการซื้อขาย: {3} \n\nคุณสามารถเลือกระหว่างสองตัวเลือกเมื่อลงทุนการซื้อขายของคุณ: \n- ใช้กระเป๋าสตางค์ Haveno ของคุณ (สะดวก แต่ธุรกรรมอาจเชื่อมโยงกันได้) หรือ\n- โอนเงินจากแหล่งเงินภายนอก (อาจเป็นส่วนตัวมากขึ้น) \n\nคุณจะเห็นตัวเลือกและรายละเอียดการลงทุนทั้งหมดหลังจากปิดป๊อปอัปนี้ +takeOffer.takeOfferFundWalletInfo.msg=คุณต้องฝากเงิน {0} เพื่อรับข้อเสนอนี้。\n\nจำนวนเงินคือผลรวมของ:\n{1}- เงินมัดจำของคุณ: {2}\n- ค่าธรรมเนียมการซื้อขาย: {3} takeOffer.alreadyPaidInFunds=หากคุณได้ชำระเงินแล้วคุณสามารถถอนเงินออกได้ในหน้าจอ \"เงิน / ส่งเงิน \" takeOffer.paymentInfo=ข้อมูลการชำระเงิน takeOffer.setAmountPrice=ตั้งยอดจำนวน @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=ธุรกรรมเงินฝากได้รับการเผยแพร่แล้ว\n{0} ต้องรอการยืนยันของบล็อกเชนอย่างน้อยหนึ่งครั้งก่อนที่จะเริ่มการชำระเงิน +portfolio.pending.step1.info.you=ธุรกรรมการฝากเงินได้รับการเผยแพร่แล้ว\nคุณต้องรอ 10 คอนเฟิร์ม (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้น +portfolio.pending.step1.info.buyer=การทำธุรกรรมฝากเงินได้รับการเผยแพร่แล้ว。\nผู้ซื้อ XMR จำเป็นต้องรอการยืนยัน 10 ครั้ง (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้นได้ portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=ข้อกำหนดที # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=โปรดส่ง {0} โดยธนาณัติ \"US Postal Money Order \" ไปยังผู้ขาย XMR\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1030,6 +1052,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=ที่อยู่ onion ของฉัน settings.net.xmrNodesLabel=ใช้โหนดเครือข่าย Monero ที่กำหนดเอง settings.net.moneroPeersLabel=เชื่อมต่อกับเน็ตเวิร์ก peers แล้ว +settings.net.connection=การเชื่อมต่อ +settings.net.connected=เชื่อมต่อ settings.net.useTorForXmrJLabel=ใช้ Tor สำหรับเครือข่าย Monero settings.net.moneroNodesLabel=ใช้โหนดเครือข่าย Monero เพื่อเชื่อมต่อ settings.net.useProvidedNodesRadio=ใช้โหนดเครือข่าย Monero ที่ให้มา @@ -1449,6 +1473,7 @@ offerDetailsWindow.confirm.maker=ยืนยัน: ยื่นข้อเส offerDetailsWindow.confirm.taker=ยืนยัน: รับข้อเสนอไปยัง {0} บิทคอยน์ offerDetailsWindow.creationDate=วันที่สร้าง offerDetailsWindow.makersOnion=ที่อยู่ onion ของผู้สร้าง +offerDetailsWindow.challenge=รหัสผ่านสำหรับข้อเสนอ qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1489,7 +1514,7 @@ tacWindow.disagree=ฉันไม่เห็นด้วยและออก tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=ซื้อขาย -tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท: +tradeDetailsWindow.disputedPayoutTxId=รหัส ID ธุรกรรมการจ่ายเงินที่พิพาท tradeDetailsWindow.tradeDate=วันที่ซื้อขาย tradeDetailsWindow.txFee=ค่าธรรมเนียมการขุด tradeDetailsWindow.tradePeersOnion=ที่อยู่ของ onion คู่ค้า @@ -1562,7 +1587,7 @@ popup.headline.error=ผิดพลาด popup.doNotShowAgain=ไม่ต้องแสดงอีกครั้ง popup.reportError.log=เปิดไฟล์ที่บันทึก popup.reportError.gitHub=รายงานไปที่ตัวติดตามปัญหา GitHub -popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/bisq-network/bisq/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. +popup.reportError={0}\n\nTo help us to improve the software please report this bug by opening a new issue at https://github.com/haveno-dex/haveno/issues.\nThe above error message will be copied to the clipboard when you click either of the buttons below.\nIt will make debugging easier if you include the haveno.log file by pressing "Open log file", saving a copy, and attaching it to your bug report. popup.error.tryRestart=โปรดลองเริ่มแอปพลิเคชั่นของคุณใหม่และตรวจสอบการเชื่อมต่อเครือข่ายของคุณเพื่อดูว่าคุณสามารถแก้ไขปัญหาได้หรือไม่ popup.error.takeOfferRequestFailed=เกิดข้อผิดพลาดขึ้นเมื่อมีคนพยายามรับข้อเสนอของคุณ: \n{0} @@ -1605,6 +1630,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. 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.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. @@ -1672,6 +1698,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=ไม่ต้องมีเงินมัดจำจากผู้ซื้อ +popup.info.buyerAsTakerWithoutDeposit=ข้อเสนอของคุณจะไม่ต้องการเงินมัดจำหรือค่าธรรมเนียมจากผู้ซื้อ XMR\n\nในการยอมรับข้อเสนอของคุณ คุณต้องแบ่งปันรหัสผ่านกับคู่ค้าการค้าของคุณภายนอก Haveno\n\nรหัสผ่านจะถูกสร้างโดยอัตโนมัติและแสดงในรายละเอียดข้อเสนอหลังจากการสร้าง + #################################################################### # Notifications #################################################################### @@ -1797,11 +1826,14 @@ navigation.support=\"ช่วยเหลือและสนับสนุ formatter.formatVolumeLabel={0} จำนวนยอด{1} formatter.makerTaker=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} +formatter.makerTaker.locked=ผู้สร้าง เป็น {0} {1} / ผู้รับเป็น {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=คุณคือ {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=คุณกำลังสร้างข้อเสนอให้ {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=คุณกำลังสร้างข้อเสนอให้ {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=คุณกำลังสร้างข้อเสนอให้กับ {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} ในฐานะผู้สร้าง formatter.asTaker={0} {1} ในฐานะคนรับ @@ -1944,17 +1976,15 @@ payment.accountType=ประเภทบัญชี payment.checking=การตรวจสอบ payment.savings=ออมทรัพย์ payment.personalId=รหัส ID ประจำตัวบุคคล -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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 แล้ว # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo @@ -1962,12 +1992,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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 มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ +payment.venmo.info=โปรดทราบว่า Venmo มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ +payment.paypal.info=โปรดทราบว่า PayPal มีความเสี่ยงในการเรียกเงินคืนสูงกว่าการโอนเงินผ่านธนาคารส่วนใหญ่ + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=ข้อมูลติดต่อ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=ข้อมูลติดต่อ @@ -1976,11 +2008,13 @@ payment.f2f.city=เมืองสำหรับการประชุมแ payment.f2f.city.prompt=ชื่อเมืองจะแสดงพร้อมกับข้อเสนอ payment.shared.optionalExtra=ข้อมูลตัวเลือกเพิ่มเติม payment.shared.extraInfo=ข้อมูลเพิ่มเติม -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=ข้อมูลเพิ่มเติมเกี่ยวกับข้อเสนอ +payment.shared.extraInfo.prompt.paymentAccount=กำหนดคำศัพท์ เงื่อนไข หรือรายละเอียดพิเศษใดๆ ที่คุณต้องการให้แสดงพร้อมข้อเสนอของคุณสำหรับบัญชีการชำระเงินนี้ (ผู้ใช้จะเห็นข้อมูลนี้ก่อนที่จะยอมรับข้อเสนอ) +payment.shared.extraInfo.prompt.offer=กำหนดเงื่อนไขพิเศษ ข้อกำหนด หรือรายละเอียดใด ๆ ที่คุณต้องการแสดงพร้อมกับข้อเสนอของคุณ +payment.shared.extraInfo.noDeposit=รายละเอียดการติดต่อและเงื่อนไขข้อเสนอ payment.f2f.info.openURL=เปิดหน้าเว็บ payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=ข้อมูลเพิ่มเติม: {0} +payment.shared.extraInfo.tooltip=ข้อมูลเพิ่มเติม: {0} payment.japan.bank=ธนาคาร payment.japan.branch=Branch @@ -1989,7 +2023,10 @@ 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://haveno.exchange/wiki/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\ + เพื่อความปลอดภัยของธุรกรรมและรองรับการระงับข้อพิพาท โปรดใช้วิธีการชำระเงินที่มีบันทึกการทำธุรกรรมที่ตรวจสอบได้ # We use constants from the code so we do not use our normal naming convention @@ -2069,7 +2106,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2121,7 +2158,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index a9b123b4d3..28034c72a3 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -43,6 +43,8 @@ shared.buyMonero=Monero Satın Al shared.sellMonero=Monero Sat shared.buyCurrency={0} satın al shared.sellCurrency={0} sat +shared.buyCurrency.locked={0} satın al 🔒 +shared.sellCurrency.locked={0} sat 🔒 shared.buyingXMRWith={0} ile XMR satın alınıyor shared.sellingXMRFor={0} karşılığında XMR satılıyor shared.buyingCurrency={0} satın alınıyor (XMR satılıyor) @@ -101,13 +103,12 @@ shared.XMRMinMax=XMR (min - max) shared.removeOffer=Teklifi kaldır shared.dontRemoveOffer=Teklifi kaldırma shared.editOffer=Teklifi düzenle -shared.duplicateOffer=Teklifi çoğalt shared.openLargeQRWindow=Büyük QR kodu penceresini aç shared.chooseTradingAccount=İşlem hesabını seç shared.faq=SSS sayfasını ziyaret et shared.yesCancel=Evet, iptal et shared.nextStep=Sonraki adım -shared.fundFromSavingsWalletButton=Haveno cüzdanından fon transfer et +shared.fundFromSavingsWalletButton=Haveno cüzdanından fonları uygula shared.fundFromExternalWalletButton=Fonlama için harici cüzdanını aç shared.openDefaultWalletFailed=Bir Monero cüzdan uygulaması açılamadı. Yüklü olduğundan emin misiniz? shared.belowInPercent=Piyasa fiyatının altında % @@ -132,7 +133,7 @@ shared.noDateAvailable=Geçerli tarih yok shared.noDetailsAvailable=Geçerli detay yok shared.notUsedYet=Henüz kullanılmadı shared.date=Tarih -shared.sendFundsDetailsWithFee=Gönderiliyor: {0}\nAlıcı adresine: {1}.\nGerekli madencilik ücreti: {2}\n\nAlıcı alacak: {3}\n\nBu miktarı çekmek istediğinizden emin misiniz? +shared.sendFundsDetailsWithFee=Gönderilen: {0}\n\nAlıcı adresi: {1}\n\nEk madenci ücreti: {2}\n\nBu tutarı göndermek istediğinizden emin misiniz? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno, bu işlemin minimum toz eşiğinin altında bir değişim çıktısı oluşturacağını (ve bu nedenle Monero konsensüs kuralları tarafından izin verilmediğini) tespit etti. Bunun yerine, bu toz ({0} satoshi{1}) madencilik ücretine eklenecektir.\n\n\n shared.copyToClipboard=Panoya kopyala @@ -150,6 +151,7 @@ shared.addNewAccount=Yeni hesap ekle shared.ExportAccounts=Hesapları Dışa Aktar shared.importAccounts=Hesapları İçe Aktar shared.createNewAccount=Yeni hesap oluştur +shared.createNewAccountDescription=Hesap bilgileriniz yerel olarak cihazınızda saklanır ve yalnızca ticaret ortağınızla ve bir anlaşmazlık açılırsa hakemle paylaşılır. shared.saveNewAccount=Yeni hesabı kaydet shared.selectedAccount=Seçilen hesap shared.deleteAccount=Hesabı sil @@ -197,13 +199,14 @@ shared.total=Toplam shared.totalsNeeded=Gereken fonlar shared.tradeWalletAddress=İşlem cüzdan adresi shared.tradeWalletBalance=İşlem cüzdan bakiyesi -shared.reserveExactAmount=Sadece gerekli fonları ayırın. Teklifinizin aktif olması için madencilik ücreti ve 10 onay (yaklaşık 20 dakika) gerekebilir. +shared.reserveExactAmount=Yalnızca gerekli fonları ayırın. Teklifinizin aktif hale gelmesi için bir madencilik ücreti ve yaklaşık 20 dakika gereklidir. shared.makerTxFee=Yapıcı: {0} shared.takerTxFee=Alıcı: {0} shared.iConfirm=Onaylıyorum shared.openURL={0}'i aç shared.fiat=Fiat shared.crypto=Kripto +shared.preciousMetals=Değerli Madenler shared.traditional=Nakit shared.otherAssets=diğer varlıklar shared.other=Diğer @@ -245,8 +248,8 @@ shared.taker=Alıcı #################################################################### mainView.menu.market=Piyasa -mainView.menu.buy=Satın Al -mainView.menu.sell=Sat +mainView.menu.buyXmr=XMR Satın Al +mainView.menu.sellXmr=XMR Sat mainView.menu.portfolio=Portföy mainView.menu.funds=Fonlar mainView.menu.support=Destek @@ -344,6 +347,7 @@ market.trades.showVolumeInUSD=Hacmi USD olarak göster offerbook.createOffer=Teklif oluştur offerbook.takeOffer=Teklif al offerbook.takeOffer.createAccount=Hesap oluştur ve teklifi al +offerbook.takeOffer.enterChallenge=Teklif şifresini girin offerbook.trader=Yatırımcı offerbook.offerersBankId=Yapıcının banka kimliği (BIC/SWIFT): {0} offerbook.offerersBankName=Yapıcının banka adı: {0} @@ -355,6 +359,8 @@ offerbook.availableOffersToSell={0} için {1} sat offerbook.filterByCurrency=Para birimini seç offerbook.filterByPaymentMethod=Ödeme yöntemini seç offerbook.matchingOffers=Uygun Teklif +offerbook.filterNoDeposit=Depozito yok +offerbook.noDepositOffers=Depozitosuz teklifler (şifre gereklidir) offerbook.timeSinceSigning=Hesap bilgisi offerbook.timeSinceSigning.info.arbitrator=bir hakem tarafından imzalandı ve eş hesaplarını imzalayabilir offerbook.timeSinceSigning.info.peer=bir eş tarafından imzalandı, limitlerin kaldırılması için %d gün bekleniyor @@ -372,6 +378,8 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=imzalı bir hesaptan XMR al offerbook.timeSinceSigning.tooltip.checkmark.wait=minimal {0} gün bekleyin offerbook.timeSinceSigning.tooltip.learnMore=Daha fazla bilgi edin offerbook.xmrAutoConf=Otomatik onay etkin mi +offerbook.buyXmrWith=XMR satın al: +offerbook.sellXmrFor=XMR'i şunlar için satın: offerbook.timeSinceSigning.help=Bir imzalı ödeme hesabı olan bir eş ile başarılı bir şekilde işlem yaptığınızda, ödeme hesabınız imzalanır.\n\ {0} gün sonra, başlangıç limiti {1} kaldırılır ve hesabınız diğer eşlerin ödeme hesaplarını imzalayabilir. @@ -386,7 +394,7 @@ offerbook.volume={0} (min - maks) offerbook.deposit=Mevduat XMR (%) offerbook.deposit.help=Her yatırımcı tarafından işlemi garanti altına almak için ödenen mevduat. İşlem tamamlandığında geri verilecektir. -offerbook.createNewOffer=teklif aç {0} {1} +offerbook.createNewOffer=Teklif oluştur {0} {1} offerbook.createOfferDisabled.tooltip=Bir seferde sadece bir teklif oluşturabilirsiniz offerbook.takeOfferButton.tooltip=Teklifi al {0} @@ -407,7 +415,7 @@ offerbook.warning.counterpartyTradeRestrictions=Karşı taraf ticaret kısıtlam offerbook.warning.newVersionAnnouncement=Bu yazılım sürümü ile, ticaret yapan eşler birbirlerinin ödeme hesaplarını doğrulayabilir ve imzalayabilir, böylece güvenilir ödeme hesapları ağı oluşturulabilir.\n\n\ Doğrulanmış ödeme hesabı olan bir eş ile başarılı bir şekilde ticaret yaptıktan sonra, ödeme hesabınız imzalanır ve ticaret limitleri belirli bir zaman aralığından sonra kaldırılır (bu aralığın uzunluğu doğrulama yöntemine bağlıdır).\n\n\ - Hesap imzalama hakkında daha fazla bilgi için, lütfen [HYPERLINK:https://haveno.exchange/wiki/Account_limits#Account_signing] belgelere bakın. + Hesap imzalama hakkında daha fazla bilgi için, lütfen [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits/#account-signing] belgelere bakın. popup.warning.tradeLimitDueAccountAgeRestriction.seller=İzin verilen ticaret miktarı, aşağıdaki kriterlere dayanan güvenlik kısıtlamaları nedeniyle {0} ile sınırlıdır:\n\ - Alıcının hesabı bir hakem veya eş tarafından imzalanmamış\n\ @@ -491,10 +499,10 @@ createOffer.createOfferFundWalletInfo.headline=Teklifinizi finanse edin # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Ticaret miktarı: {0} \n createOffer.createOfferFundWalletInfo.msg=Bu teklife {0} yatırmanız gerekiyor.\n\n\ - Bu fonlar yerel cüzdanınızda ayrılır ve birisi teklifinizi aldığında multisig bir cüzdana kilitlenir.\n\n\ - Miktar, şunların toplamıdır:\n\ + Bu fonlar yerel cüzdanınızda rezerve edilir ve birisi teklifinizi kabul ettiğinde bir multisig cüzdanda kilitlenir.\n\n\ + Tutarın toplamı şudur:\n\ {1}\ - - Güvenlik teminatınız: {2}\n\ + - Güvenlik depozitonuz: {2}\n\ - İşlem ücreti: {3} # only first part "Bir teklif verirken bir hata oluştu:" has been used before. We added now the rest (need update in existing translations!) @@ -520,7 +528,10 @@ createOffer.setDepositAsBuyer=Alıcı olarak benim güvenlik teminatımı ayarla createOffer.setDepositForBothTraders=Tüccarların güvenlik teminatı (%) createOffer.securityDepositInfo=Alıcının güvenlik teminatı {0} olacak createOffer.securityDepositInfoAsBuyer=Alıcı olarak güvenlik teminatınız {0} olacak -createOffer.minSecurityDepositUsed=Minimum alıcı güvenlik teminatı kullanıldı +createOffer.minSecurityDepositUsed=Minimum güvenlik depozitosu kullanılır +createOffer.buyerAsTakerWithoutDeposit=Alıcıdan depozito gerekmez (şifre korumalı) +createOffer.myDeposit=Güvenlik depozitam (%) +createOffer.myDepositInfo=Güvenlik depozitonuz {0} olacaktır. #################################################################### @@ -546,6 +557,8 @@ takeOffer.fundsBox.networkFee=Toplam madencilik ücretleri takeOffer.fundsBox.takeOfferSpinnerInfo=Teklif alınıyor: {0} takeOffer.fundsBox.paymentLabel=ID {0} ile Haveno işlemi takeOffer.fundsBox.fundsStructure=({0} güvenlik teminatı, {1} işlem ücreti) +takeOffer.fundsBox.noFundingRequiredTitle=Fonlama gerekmez +takeOffer.fundsBox.noFundingRequiredDescription=Bu teklifi almak için satıcıdan passphrase'i Haveno dışında alınız. takeOffer.success.headline=Teklifi başarıyla aldınız. takeOffer.success.info=İşleminizin durumunu \"Portföy/Açık işlemler\" kısmında görebilirsiniz. takeOffer.error.message=Teklif alımı sırasında bir hata oluştu.\n\n{0} @@ -557,7 +570,7 @@ takeOffer.noPriceFeedAvailable=Bu teklifi alamazsınız çünkü piyasa fiyatın takeOffer.takeOfferFundWalletInfo.headline=İşleminizi finanse edin # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- İşlem miktarı: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Bu teklifi almak için {0} yatırmanız gerekiyor.\n\nBu miktar şunların toplamıdır:\n{1}- Güvenlik teminatınız: {2}\n- İşlem ücreti: {3} +takeOffer.takeOfferFundWalletInfo.msg=Bu teklifi kabul etmek için {0} yatırmanız gerekiyor.\n\nMiktar, şu kalemlerin toplamıdır:\n{1}- Güvenlik depozitonuz: {2}\n- İşlem ücreti: {3} takeOffer.alreadyPaidInFunds=Eğer zaten fon yatırdıysanız, \"Fonlar/Fon gönder\" ekranında çekebilirsiniz. takeOffer.setAmountPrice=Miktar ayarla takeOffer.alreadyFunded.askCancel=Bu teklifi zaten finanse ettiniz.\nŞimdi iptal ederseniz, fonlarınız yerel Haveno cüzdanınızda kalacak ve \"Fonlar/Fon gönder\" ekranında çekilebilir olacaktır.\nİptal etmek istediğinizden emin misiniz? @@ -655,7 +668,8 @@ portfolio.pending.autoConf.state.ERROR=Bir hizmet talebinde hata oluştu. Otomat # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Bir hizmet başarısızlıkla sonuçlandı. Otomatik onay mümkün değil. -portfolio.pending.step1.info=Yatırım işlemi yayımlandı.\n{0} ödemeye başlamadan önce 10 onay (yaklaşık 20 dakika) beklemeniz gerekiyor. +portfolio.pending.step1.info.you=Depozito işlemi yayımlandı.\nÖdemenin başlayabilmesi için 10 onay beklemeniz gerekiyor (yaklaşık 20 dakika). +portfolio.pending.step1.info.buyer=Depozito işlemi yayınlandı.\nXMR alıcısının ödeme işlemine başlanmadan önce 10 onay beklemesi gerekiyor (yaklaşık 20 dakika). portfolio.pending.step1.warn=Yatırım işlemi henüz onaylanmadı. Bu genellikle yaklaşık 20 dakika sürer, ancak ağ yoğunsa daha uzun sürebilir. portfolio.pending.step1.openForDispute=Yatırım işlemi hala onaylanmadı. \ 20 dakikadan çok daha uzun süre beklediyseniz, Haveno desteği ile iletişime geçin. @@ -670,7 +684,7 @@ portfolio.pending.step2_buyer.refTextWarn=Önemli: ödeme yaparken, \"ödeme ned portfolio.pending.step2_buyer.fees=Bankanız transfer yapmak için sizden herhangi bir ücret alıyorsa, bu ücretleri ödemekten siz sorumlusunuz. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.fees.swift=SWIFT ödemesini göndermek için SHA (paylaşılan ücret modeli) kullanmanız gerekmektedir. \ - Daha fazla ayrıntı için [HYPERLINK:https://haveno.exchange/wiki/SWIFT#Use_the_correct_fee_option] adresine bakınız. + Daha fazla ayrıntı için [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT#Use_the_correct_fee_option] adresine bakınız. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.crypto=Lütfen dış {0} cüzdanınızdan\n{1} XMR satıcısına transfer yapın.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -690,7 +704,7 @@ portfolio.pending.step2_buyer.postal=Lütfen "US Postal Money Order" kullanarak # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.payByMail=Lütfen "Pay by Mail" kullanarak {0} tutarını XMR satıcısına gönderin. \ Belirli talimatlar işlem sözleşmesinde bulunmaktadır, veya belirsizse trader sohbeti aracılığıyla sorular sorabilirsiniz. \ - Pay by Mail hakkında daha fazla ayrıntı için Haveno wiki [HYPERLINK:https://haveno.exchange/wiki/Cash_by_Mail] adresine bakın.\n\n + Pay by Mail hakkında daha fazla ayrıntı için Haveno wiki [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Pay_By_Mail] adresine bakın.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.pay=Lütfen belirtilen ödeme yöntemini kullanarak {0} tutarını XMR satıcısına ödeyin. Satıcının hesap bilgilerini bir sonraki ekranda bulacaksınız.\n\n # suppress inspection "TrailingSpacesInProperty" @@ -945,7 +959,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı 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/bisq-network/support/issues]\n\n\ + [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.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ @@ -955,7 +969,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=G (satıcı da tam ticaret miktarını geri alır). \ Bu şekilde, güvenlik riski yoktur ve yalnızca ticaret ücretleri kaybedilir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Eğer alıcı da gecikmiş ödeme işlemini eksikse, onlara ödemeyi göndermemeleri ve \ @@ -964,18 +978,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= tamamını geri almasını önermelidir (satıcı da tam ticaret miktarını geri alır). \ Aksi takdirde ticaret miktarı alıcıya gitmelidir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Ticaret protokolü yürütülürken bir hata oluştu.\n\n\ Hata: {0}\n\n\ Bu hata kritik olmayabilir ve ticaret normal şekilde tamamlanabilir. Emin değilseniz, \ Haveno arabulucularından tavsiye almak için bir arabuluculuk bileti açın. \n\n\ Eğer hata kritikse ve ticaret tamamlanamazsa, ticaret ücretinizi kaybetmiş olabilirsiniz. \ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Ticaret sözleşmesi ayarlanmadı.\n\n\ Ticaret tamamlanamaz ve ticaret ücretinizi kaybetmiş olabilirsiniz. \ Eğer öyleyse, kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Ticaret protokolü bazı sorunlarla karşılaştı.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Ticaret protokolü ciddi bir sorunla karşılaştı.\n\n{0}\n\n\ Ticareti başarısız ticaretler arasına taşımak ister misiniz?\n\n\ @@ -1306,6 +1320,8 @@ settings.net.p2pHeader=Haveno ağı settings.net.onionAddressLabel=Onion adresim settings.net.xmrNodesLabel=Özel Monero düğümleri kullan settings.net.moneroPeersLabel=Bağlı eşler +settings.net.connection=Bağlantı +settings.net.connected=Bağlı settings.net.useTorForXmrJLabel=Monero ağı için Tor kullan settings.net.useTorForXmrAfterSyncRadio=Cüzdan senkronize edildikten sonra settings.net.useTorForXmrOffRadio=Asla @@ -1957,6 +1973,7 @@ offerDetailsWindow.confirm.taker=Onayla: {0} monero teklifi al offerDetailsWindow.confirm.takerCrypto=Onayla: {0} {1} teklifi al offerDetailsWindow.creationDate=Oluşturma tarihi offerDetailsWindow.makersOnion=Yapıcı'nın onion adresi +offerDetailsWindow.challenge=Teklif şifresi 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. @@ -2005,7 +2022,7 @@ tacWindow.disagree=Kabul etmiyorum ve çıkıyorum tacWindow.arbitrationSystem=Uyuşmazlık çözümü tradeDetailsWindow.headline=Ticaret -tradeDetailsWindow.disputedPayoutTxId=Uyuşmazlık konusu olan ödeme işlem kimliği: +tradeDetailsWindow.disputedPayoutTxId=Uyuşmazlık konusu olan ödeme işlem kimliği tradeDetailsWindow.tradeDate=Ticaret tarihi tradeDetailsWindow.txFee=Madencilik ücreti tradeDetailsWindow.tradePeersOnion=Ticaret ortaklarının onion adresi @@ -2143,7 +2160,7 @@ popup.warning.seed=anahtar kelime popup.warning.mandatoryUpdate.trading=Lütfen en son Haveno sürümüne güncelleyin. \ Eski sürümler için ticareti devre dışı bırakan zorunlu bir güncelleme yayınlandı. \ Daha fazla bilgi için lütfen Haveno Forumunu kontrol edin. -popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen Haveno ağ yöneticilerini bir filtre nesnesi kaydetmek için ctrl + f ile bilgilendirin. +popup.warning.noFilter=Tohum düğümlerinden bir filtre nesnesi almadık. Lütfen ağ yöneticilerine bir filtre nesnesi kaydetmeleri için bilgi verin. popup.warning.burnXMR=Bu işlem mümkün değil, çünkü {0} tutarındaki madencilik ücretleri, transfer edilecek {1} tutarını aşacaktır. \ Lütfen madencilik ücretleri tekrar düşük olana kadar bekleyin veya transfer etmek için daha fazla XMR biriktirin. @@ -2254,6 +2271,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkey'ler imzalandı popup.accountSigning.unsignedPubKeys.result.signed=İmzalanmış pubkey'ler popup.accountSigning.unsignedPubKeys.result.failed=İmzalama başarısız oldu +popup.info.buyerAsTakerWithoutDeposit.headline=Alıcıdan depozito gerekmez +popup.info.buyerAsTakerWithoutDeposit=Teklifiniz, XMR alıcısından güvenlik depozitosu veya ücret talep etmeyecektir.\n\nTeklifinizi kabul etmek için, ticaret ortağınızla Haveno dışında bir şifre paylaşmalısınız.\n\nŞifre otomatik olarak oluşturulur ve oluşturulduktan sonra teklif detaylarında görüntülenir. + popup.info.torMigration.msg=Haveno düğümünüz muhtemelen eski bir Tor v2 adresi kullanıyor. \ Lütfen Haveno düğümünüzü bir Tor v3 adresine geçirin. \ Önceden veri dizininizi yedeklediğinizden emin olun. @@ -2393,11 +2413,14 @@ navigation.support="Destek" formatter.formatVolumeLabel={0} miktar{1} formatter.makerTaker=Yapan olarak {0} {1} / Alan olarak {2} {3} +formatter.makerTaker.locked=Yapıcı olarak {0} {1} / Alan olarak {2} {3} 🔒 formatter.youAreAsMaker=Yapan sizsiniz: {1} {0} (maker) / Alan: {3} {2} formatter.youAreAsTaker=Alan sizsiniz: {1} {0} (taker) / Yapan: {3} {2} formatter.youAre=Şu anda sizsiniz {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Şu anda bir teklif oluşturuyorsunuz: {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Şu anda bir teklif oluşturuyorsunuz: {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} olarak formatter.asTaker={0} {1} olarak @@ -2638,7 +2661,7 @@ payment.limits.info=Lütfen tüm banka transferlerinin belirli bir miktarda geri \n\ Bu limit yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz.\n\ \n\ - Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" payment.limits.info.withSigning=Geri ödeme riskini sınırlamak için, Haveno, bu ödeme hesabı türü için işlem başına limitler belirler \ aşağıdaki 2 faktöre dayanır:\n\n\ @@ -2656,7 +2679,7 @@ payment.limits.info.withSigning=Geri ödeme riskini sınırlamak için, Haveno, \n\ Bu limitler yalnızca tek bir işlemin boyutuna uygulanır—istediğiniz kadar işlem yapabilirsiniz. \n\ \n\ - Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. + Daha fazla ayrıntı için wiki sayfasına bakın [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. payment.cashDeposit.info=Lütfen bankanızın başka kişilerin hesaplarına nakit yatırma işlemlerine izin verdiğini onaylayın. \ Örneğin, Bank of America ve Wells Fargo gibi bankalar bu tür yatırımlara artık izin vermemektedir. @@ -2668,9 +2691,9 @@ payment.account.revolut.addUserNameInfo={0}\n\ Bu, hesap yaş imza durumunuzu etkilemeyecektir. payment.revolut.addUserNameInfo.headLine=Revolut hesabını güncelle -payment.cashapp.info=Cash App, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. Cash App ile ticaret yaparken bunun farkında olun. -payment.venmo.info=Venmo, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. Venmo ile ticaret yaparken bunun farkında olun. -payment.paypal.info=PayPal, çoğu banka transferine göre daha yüksek geri ödeme riskine sahiptir. PayPal ile ticaret yaparken bunun farkında olun. +payment.cashapp.info=Lütfen Cash App'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. +payment.venmo.info=Lütfen Venmo'nun çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. +payment.paypal.info=Lütfen PayPal'in çoğu banka transferinden daha yüksek geri ödeme riski taşıdığını unutmayın. payment.amazonGiftCard.upgrade=Amazon hediye kartları ödeme yöntemi için ülkenin belirtilmesi gerekmektedir. payment.account.amazonGiftCard.addCountryInfo={0}\n\ @@ -2686,20 +2709,20 @@ payment.swift.info.account=Haveno'da SWIFT kullanımı için temel yönergeleri - alıcı, paylaşılan ücret modeli (SHA) kullanarak ödeme yapmalıdır \n\ - alıcı ve satıcı ücretlerle karşılaşabilir, bu yüzden bankalarının ücret tarifelerini önceden kontrol etmelidirler \n\ \n\ -SWIFT, diğer ödeme yöntemlerinden daha karmaşıktır, bu yüzden lütfen wiki'deki tam rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +SWIFT, diğer ödeme yöntemlerinden daha karmaşıktır, bu yüzden lütfen wiki'deki tam rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.buyer=SWIFT ile monero satın almak için şunları yapmalısınız:\n\ \n\ - ödeme yapanın belirttiği para biriminde ödeme yapın \n\ - ödeme göndermek için paylaşılan ücret modeli (SHA) kullanın\n\ \n\ -Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.swift.info.seller=SWIFT gönderenler, ödemeleri göndermek için paylaşılan ücret modeli (SHA) kullanmak zorundadır.\n\ \n\ SHA kullanmayan bir SWIFT ödemesi alırsanız, bir arabuluculuk bileti açın.\n\ \n\ -Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://haveno.exchange/wiki/SWIFT]. +Ceza almamak ve sorunsuz ticaretler yapmak için lütfen wiki'deki daha fazla rehberi inceleyin [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/SWIFT]. payment.imps.info.account=Lütfen şunları dahil ettiğinizden emin olun:\n\n\ ● Hesap sahibi tam adı\n\ @@ -2931,14 +2954,14 @@ payment.strike.info.seller=Ödemenin XMR Alıcısının Haveno'da sağlanan Stri Bu limitleri aşarsanız, işleminiz iptal edilebilir ve ceza uygulanabilir. payment.transferwiseUsd.info.account=ABD bankacılık düzenlemeleri nedeniyle, USD ödemeleri göndermek ve almak, çoğu diğer \ -para biriminden daha fazla sınırlamaya tabidir. Bu nedenle USD, Haveno TransferWise ödeme yöntemine eklenmemiştir.\n\n\ - TransferWise-USD ödeme yöntemi, Haveno kullanıcılarının USD cinsinden ticaret yapmasına olanak tanır.\n\n\ -Wise, eski adıyla TransferWise hesabı olan herkes, Haveno'da TransferWise-USD'yi bir ödeme yöntemi olarak ekleyebilir. \ +para biriminden daha fazla sınırlamaya tabidir. Bu nedenle USD, Haveno Wise ödeme yöntemine eklenmemiştir.\n\n\ + Wise-USD ödeme yöntemi, Haveno kullanıcılarının USD cinsinden ticaret yapmasına olanak tanır.\n\n\ +Wise, eski adıyla Wise hesabı olan herkes, Haveno'da Wise-USD'yi bir ödeme yöntemi olarak ekleyebilir. \ Bu, USD ile XMR alıp satmalarını sağlar.\n\n\ Haveno'da ticaret yaparken XMR Alıcıları, ödeme nedeni için herhangi bir referans kullanmamalıdır. Ödeme nedeni \ - gerekliyse, yalnızca TransferWise-USD hesap sahibinin tam adını kullanmalıdırlar. -payment.transferwiseUsd.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno TransferWise-USD hesabındaki e-posta adresine gönderin. -payment.transferwiseUsd.info.seller=Alınan ödemenin, Haveno'daki TransferWise-USD hesabındaki XMR Alıcısının adıyla eşleştiğini kontrol edin. + gerekliyse, yalnızca Wise-USD hesap sahibinin tam adını kullanmalıdırlar. +payment.transferwiseUsd.info.buyer=Ödemeyi yalnızca XMR Satıcısının Haveno Wise-USD hesabındaki e-posta adresine gönderin. +payment.transferwiseUsd.info.seller=Alınan ödemenin, Haveno'daki Wise-USD hesabındaki XMR Alıcısının adıyla eşleştiğini kontrol edin. payment.usPostalMoneyOrder.info=Haveno'da ABD Posta Havale Emirleri (USPMO) kullanarak ticaret yapmak için aşağıdakileri anladığınızdan emin olmanız gerekmektedir:\n\ \n\ @@ -2978,9 +3001,9 @@ payment.payByMail.extraInfo.prompt=Tekliflerinize lütfen şunları belirtin: \n Diğer detaylar. payment.tradingRestrictions=Lütfen yapıcı tarafın şartlarını ve koşullarını gözden geçirin.\n\ Gereksinimleri karşılamıyorsanız bu ticareti yapmayın. -payment.cashAtAtm.info=ATM'de Nakit: Kod kullanarak ATM'den kartsız para çekme\n\n\ +payment.cashAtAtm.info=Kartsız Nakit: Kod kullanarak ATM'den kartsız para çekme\n\n\ Bu ödeme yöntemini kullanmak için:\n\n\ - 1. Kabul ettiğiniz bankaları, bölgeleri veya teklifle birlikte gösterilecek diğer şartları listeleyerek bir ATM'de Nakit ödeme hesabı oluşturun.\n\n\ + 1. Kabul ettiğiniz bankaları, bölgeleri veya teklifle birlikte gösterilecek diğer şartları listeleyerek bir Kartsız Nakit ödeme hesabı oluşturun.\n\n\ 2. Ödeme hesabı ile bir teklif oluşturun veya bir teklifi kabul edin.\n\n\ 3. Teklif kabul edildiğinde, ödeme detaylarını paylaşmak ve ödemeyi tamamlamak için eşinizle sohbet edin.\n\n\ Ticaret sözleşmenizde belirtilen şekilde işlemi tamamlayamazsanız, güvenlik teminatınızın bir kısmını (veya tamamını) kaybedebilirsiniz. @@ -2994,7 +3017,10 @@ payment.f2f.city='Yüz yüze' buluşma için şehir payment.f2f.city.prompt=Şehir teklifle birlikte gösterilecektir payment.shared.optionalExtra=İsteğe bağlı ek bilgi payment.shared.extraInfo=Ek bilgi -payment.shared.extraInfo.prompt=Bu ödeme hesabınız için tekliflerinize eklemek istediğiniz özel şart, koşul veya detayları tanımlayın (kullanıcılar bu bilgileri teklifleri kabul etmeden önce görecektir). +payment.shared.extraInfo.offer=Ek teklif bilgileri +payment.shared.extraInfo.prompt.paymentAccount=Bu ödeme hesabınız için tekliflerinize eklemek istediğiniz özel şart, koşul veya detayları tanımlayın (kullanıcılar bu bilgileri teklifleri kabul etmeden önce görecektir). +payment.shared.extraInfo.prompt.offer=Teklifinizle birlikte göstermek istediğiniz özel terimleri, koşulları veya detayları tanımlayın. +payment.shared.extraInfo.noDeposit=İletişim detayları ve teklif şartları payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimiçi işlemlerden farklı riskler içerir.\n\n\ Başlıca farklar şunlardır:\n\ ● Ticaret eşleri, sağlanan iletişim bilgilerini kullanarak buluşma yeri ve zamanını paylaşmalıdır.\n\ @@ -3005,10 +3031,10 @@ payment.f2f.info='Yüz Yüze' ticaretler farklı kurallara sahiptir ve çevrimi yardımcı olamaz. Bu tür durumlarda XMR fonları süresiz olarak veya ticaret eşleri anlaşmaya varana \ kadar kilitlenebilir.\n\n\ 'Yüz Yüze' ticaretlerin farklarını tam olarak anladığınızdan emin olmak için lütfen şu adresteki talimatları \ - ve tavsiyeleri okuyun: [HYPERLINK:https://haveno.exchange/wiki/Face-to-face_(payment_method)] + ve tavsiyeleri okuyun: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/F2F] payment.f2f.info.openURL=Web sayfasını aç payment.f2f.offerbook.tooltip.countryAndCity=Ülke ve şehir: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Ek bilgi: {0} +payment.shared.extraInfo.tooltip=Ek bilgi: {0} payment.ifsc=IFS Kodu payment.ifsc.validation=IFSC formatı: XXXX0999999 @@ -3022,12 +3048,15 @@ payment.payid.info=PayID, telefon numarası, e-posta adresi veya Avustralya İş toplum hesabınıza güvenli bir şekilde bağlayabileceğiniz bir kimliktir. Avustralya finans kurumunuzla zaten bir PayID oluşturmuş \ olmanız gerekmektedir. Hem gönderici hem de alıcı finans kurumlarının PayID'yi desteklemesi gerekmektedir. payment.amazonGiftCard.info=Amazon eGift Kart ile ödeme yapmak için, Amazon hesabınız aracılığıyla bir Amazon eGift Kartı'nı XMR satıcısına göndermeniz gerekecek. \n\n\ - Daha fazla ayrıntı ve en iyi uygulamalar için lütfen wiki'ye bakın: [HYPERLINK:https://haveno.exchange/wiki/Amazon_eGift_card] \n\n\ + Daha fazla ayrıntı ve en iyi uygulamalar için lütfen wiki'ye bakın: [HYPERLINK:https://docs.haveno.exchange/the-project/payment_methods/Amazon_eGift_card] \n\n\ Üç önemli not:\n\ - Amazon'un daha büyük hediye kartlarını sahtekarlık olarak işaretlediği bilindiğinden, 100 USD veya daha küçük tutarlarda hediye kartları göndermeye çalışın\n\ - hediye kartının mesajı için yaratıcı, inanılabilir bir metin kullanmaya çalışın (örneğin, "Doğum günün kutlu olsun Metin Torun!") ve ticaret \ kimliğini ekleyin (ve ticaret sohbetinde ticaret eşinize seçtiğiniz referans metnini söyleyin, böylece ödemenizi doğrulayabilirler)\n\ - Amazon eGift Kartları, yalnızca satın alındıkları Amazon web sitesinde kullanılabilir (örneğin, amazon.it üzerinden satın alınan bir hediye kartı yalnızca amazon.it üzerinde kullanılabilir) +payment.paysafe.info=Sizin korumanız için, Paysafecard PIN'lerini ödeme için kullanmanızı kesinlikle önermiyoruz.\n\n\ + PIN'ler ile yapılan işlemler, ihtilaf çözümü için bağımsız olarak doğrulanamaz. Bir sorun oluşursa, fonların geri alınması mümkün olmayabilir.\n\n\ + İhtilaf çözümü ile işlem güvenliğini sağlamak için, her zaman doğrulanabilir kayıtlar sağlayan ödeme yöntemlerini kullanın. # We use constants from the code so we do not use our normal naming convention # dynamic values are not recognized by IntelliJ @@ -3109,9 +3138,9 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD=TransferWise-USD +TRANSFERWISE_USD=Wise-USD # suppress inspection "UnusedProperty" PAYSERA=Paysera # suppress inspection "UnusedProperty" @@ -3205,9 +3234,9 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" -TRANSFERWISE_USD_SHORT=TransferWise-USD +TRANSFERWISE_USD_SHORT=Wise-USD # suppress inspection "UnusedProperty" PAYSERA_SHORT=Paysera # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 33f8bf350d..e6dd802dee 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -40,6 +40,8 @@ shared.buyMonero=Mua monero shared.sellMonero=Bán monero shared.buyCurrency=Mua {0} shared.sellCurrency=Bán {0} +shared.buyCurrency.locked=Mua {0} 🔒 +shared.sellCurrency.locked=Bán {0} 🔒 shared.buyingXMRWith=đang mua XMR với {0} shared.sellingXMRFor=đang bán XMR với {0} shared.buyingCurrency=đang mua {0} (đang bán XMR) @@ -103,7 +105,7 @@ shared.faq=Visit FAQ page shared.yesCancel=Có, hủy shared.nextStep=Bước tiếp theo shared.selectTradingAccount=Chọn tài khoản giao dịch -shared.fundFromSavingsWalletButton=Chuyển tiền từ Ví Haveno +shared.fundFromSavingsWalletButton=Áp dụng tiền từ ví Haveno shared.fundFromExternalWalletButton=Mở ví ngoài để nộp tiền shared.openDefaultWalletFailed=Failed to open a Monero wallet application. Are you sure you have one installed? shared.belowInPercent=Thấp hơn % so với giá thị trường @@ -123,6 +125,7 @@ shared.noDateAvailable=Ngày tháng không hiển thị shared.noDetailsAvailable=Không có thông tin shared.notUsedYet=Chưa được sử dụng shared.date=Ngày +shared.sendFundsDetailsWithFee=Đang gửi: {0}\n\nĐến địa chỉ nhận: {1}\n\nPhí thợ đào bổ sung: {2}\n\nBạn có chắc chắn muốn gửi số tiền này không? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Sao chép đến clipboard @@ -139,6 +142,7 @@ shared.addNewAccount=Thêm tài khoản mới shared.ExportAccounts=Truy xuất tài khoản shared.importAccounts=Truy nhập tài khoản shared.createNewAccount=Tạo tài khoản mới +shared.createNewAccountDescription=Thông tin tài khoản của bạn được lưu trữ cục bộ trên thiết bị của bạn và chỉ được chia sẻ với đối tác giao dịch của bạn và trọng tài nếu xảy ra tranh chấp. shared.saveNewAccount=Lưu tài khoản mới shared.selectedAccount=Tài khoản được chọn shared.deleteAccount=Xóa tài khoản @@ -185,13 +189,14 @@ shared.total=Tổng shared.totalsNeeded=Số tiền cần shared.tradeWalletAddress=Địa chỉ ví giao dịch shared.tradeWalletBalance=Số dư ví giao dịch -shared.reserveExactAmount=Dự trữ chỉ số tiền cần thiết. Có thể yêu cầu một phí đào và 10 xác nhận (~20 phút) trước khi giao dịch của bạn trở nên hiệu lực. +shared.reserveExactAmount=Chỉ giữ lại số tiền cần thiết. Yêu cầu phí khai thác và khoảng 20 phút trước khi đề nghị của bạn được công khai. shared.makerTxFee=Người tạo: {0} shared.takerTxFee=Người nhận: {0} shared.iConfirm=Tôi xác nhận shared.openURL=Mở {0} shared.fiat=Tiền pháp định shared.crypto=Tiền mã hóa +shared.preciousMetals=Kim loại quý shared.all=Tất cả shared.edit=Chỉnh sửa shared.advancedOptions=Tùy chọn nâng cao @@ -328,6 +333,7 @@ offerbook.createOffer=Tạo chào giá offerbook.takeOffer=Nhận chào giá offerbook.takeOfferToBuy=Nhận chào giá mua {0} offerbook.takeOfferToSell=Nhận chào giá bán {0} +offerbook.takeOffer.enterChallenge=Nhập mật khẩu đề nghị offerbook.trader=Trader offerbook.offerersBankId=ID ngân hàng của người tạo (BIC/SWIFT): {0} offerbook.offerersBankName=Tên ngân hàng của người tạo: {0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=Các quốc gia có ngân hàng được ch offerbook.availableOffers=Các chào giá hiện có offerbook.filterByCurrency=Lọc theo tiền tệ offerbook.filterByPaymentMethod=Lọc theo phương thức thanh toán -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=Các ưu đãi phù hợp với tài khoản của tôi +offerbook.filterNoDeposit=Không đặt cọc +offerbook.noDepositOffers=Các ưu đãi không yêu cầu đặt cọc (cần mật khẩu) offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} ngày offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing 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.notSigned=Not signed yet @@ -362,6 +372,7 @@ offerbook.nrOffers=Số chào giá: {0} offerbook.volume={0} (min - max) 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.createNewOffer=Tạo ưu đãi cho {0} {1} offerbook.createOfferToBuy=Tạo chào giá mua mới {0} offerbook.createOfferToSell=Tạo chào giá bán mới {0} @@ -452,7 +463,12 @@ createOffer.placeOfferButton=Kiểm tra:: Đặt báo giá cho {0} monero 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 -createOffer.createOfferFundWalletInfo.msg=Bạn cần đặt cọc {0} cho báo giá này.\n\nCác khoản tiền này sẽ được giữ trong ví nội bộ của bạn và sẽ bị khóa vào địa chỉ đặt cọc multisig khi có người nhận báo giá của bạn.\n\nKhoản tiền này là tổng của:\n{1}- tiền gửi đại lý của bạn: {2}\n- Phí giao dịch: {3}\n- Phí đào: {4}\n\nBạn có thể chọn giữa hai phương án khi nộp tiền cho giao dịch:\n- Sử dụng ví Haveno của bạn (tiện lợi, nhưng giao dịch có thể bị kết nối) OR\n- Chuyển từ ví bên ngoài (riêng tư hơn)\n\nBạn sẽ xem các phương án nộp tiền và thông tin chi tiết sau khi đóng cửa sổ này. +createOffer.createOfferFundWalletInfo.msg=Bạn cần nạp {0} cho lời đề nghị này.\n\n\ + Số tiền này sẽ được giữ trong ví cục bộ của bạn và sẽ được khóa vào ví multisig ngay khi có người chấp nhận lời đề nghị của bạn.\n\n\ + Số tiền bao gồm:\n\ + {1}\ + - Tiền đặt cọc bảo đảm của bạn: {2}\n\ + - Phí giao dịch: {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=Có lỗi xảy ra khi đặt chào giá:\n\n{0}\n\nKhông còn tiền trong ví của bạn.\nHãy khởi động lại ứng dụng và kiểm tra kết nối mạng. @@ -474,7 +490,10 @@ createOffer.setDepositAsBuyer=Cài đặt tiền đặt cọc của tôi với v createOffer.setDepositForBothTraders=Set both traders' security deposit (%) createOffer.securityDepositInfo=Số tiền đặt cọc cho người mua của bạn sẽ là {0} createOffer.securityDepositInfoAsBuyer=Số tiền đặt cọc của bạn với vai trò người mua sẽ là {0} -createOffer.minSecurityDepositUsed=Min. buyer security deposit is used +createOffer.minSecurityDepositUsed=Khoản tiền đặt cọc bảo mật tối thiểu được sử dụng +createOffer.buyerAsTakerWithoutDeposit=Không cần đặt cọc từ người mua (được bảo vệ bằng mật khẩu) +createOffer.myDeposit=Tiền đặt cọc bảo mật của tôi (%) +createOffer.myDepositInfo=Khoản tiền đặt cọc của bạn sẽ là {0} #################################################################### @@ -498,6 +517,8 @@ takeOffer.fundsBox.networkFee=Tổng phí đào takeOffer.fundsBox.takeOfferSpinnerInfo=Chấp nhận đề xuất: {0} takeOffer.fundsBox.paymentLabel=giao dịch Haveno có ID {0} takeOffer.fundsBox.fundsStructure=({0} tiền gửi đại lý, {1} phí giao dịch, {2} phí đào) +takeOffer.fundsBox.noFundingRequiredTitle=Không cần tài trợ +takeOffer.fundsBox.noFundingRequiredDescription=Lấy mật khẩu giao dịch từ người bán ngoài Haveno để nhận đề nghị này. takeOffer.success.headline=Bạn đã nhận báo giá thành công. takeOffer.success.info=Bạn có thể xem trạng thái giao dịch của bạn tại \"Portfolio/Các giao dịch mở\". takeOffer.error.message=Có lỗi xảy ra khi nhận báo giá.\n\n{0} @@ -508,7 +529,7 @@ takeOffer.noPriceFeedAvailable=Bạn không thể nhận báo giá này do sử takeOffer.takeOfferFundWalletInfo.headline=Nộp tiền cho giao dịch của bạn # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- Giá trị giao dịch: {0} \n -takeOffer.takeOfferFundWalletInfo.msg=Bạn cần nộp {0} để nhận báo giá này.\n\nGiá trị này là tổng của:\n{1}- Tiền ứng trước của bạn: {2}\n- phí giao dịch: {3}\n\nBạn có thể chọn một trong hai phương án khi nộp tiền cho giao dịch của bạn:\n- Sử dụng ví Haveno (tiện lợi, nhưng giao dịch có thể bị kết nối) OR\n- Chuyển từ ví ngoài (riêng tư hơn)\n\nBạn sẽ thấy các phương án nộp tiền và thông tin chi tiết sau khi đóng cửa sổ này. +takeOffer.takeOfferFundWalletInfo.msg=Bạn cần phải deposit {0} để chấp nhận đề nghị này.\n\nSố tiền là tổng của:\n{1}- Khoản tiền đặt cọc của bạn: {2}\n- Phí giao dịch: {3} takeOffer.alreadyPaidInFunds=Bạn đã thanh toán, bạn có thể rút số tiền này tại màn hình \"Vốn/Gửi vốn\". takeOffer.paymentInfo=Thông tin thanh toán takeOffer.setAmountPrice=Cài đặt số tiền @@ -594,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Giao dịch đặt cọc đã được công bố.\n{0} Bạn cần đợi ít nhất một xác nhận blockchain trước khi bắt đầu thanh toán. +portfolio.pending.step1.info.you=Giao dịch nạp tiền đã được công bố.\nBạn cần đợi 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. +portfolio.pending.step1.info.buyer=Giao dịch gửi tiền đã được công bố.\nNgười mua XMR cần chờ 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -619,7 +641,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=YÊU CẦU QUAN TRỌNG:\nSau k # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=Hãy gửi {0} bằng \"Phiếu chuyển tiền US\" cho người bán XMR.\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -797,11 +819,11 @@ 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/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -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/bisq-network/support/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/bisq-network/support/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/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +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.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] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. @@ -1032,6 +1054,8 @@ settings.net.p2pHeader=Haveno network settings.net.onionAddressLabel=Địa chỉ onion của tôi settings.net.xmrNodesLabel=Sử dụng nút Monero thông dụng settings.net.moneroPeersLabel=Các đối tác được kết nối +settings.net.connection=Kết nối +settings.net.connected=Kết nối settings.net.useTorForXmrJLabel=Sử dụng Tor cho mạng Monero settings.net.moneroNodesLabel=nút Monero để kết nối settings.net.useProvidedNodesRadio=Sử dụng các nút Monero Core đã cung cấp @@ -1451,6 +1475,7 @@ 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.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 qRCodeWindow.headline=QR Code qRCodeWindow.msg=Please use this QR code for funding your Haveno wallet from your external wallet. @@ -1491,7 +1516,7 @@ tacWindow.disagree=Tôi không đồng ý và thoát tacWindow.arbitrationSystem=Dispute resolution tradeDetailsWindow.headline=giao dịch -tradeDetailsWindow.disputedPayoutTxId=ID giao dịch hoàn tiền khiếu nại: +tradeDetailsWindow.disputedPayoutTxId=ID giao dịch hoàn tiền khiếu nại tradeDetailsWindow.tradeDate=Ngày giao dịch tradeDetailsWindow.txFee=Phí đào tradeDetailsWindow.tradePeersOnion=Địa chỉ onion Đối tác giao dịch @@ -1564,7 +1589,7 @@ popup.headline.error=Lỗi popup.doNotShowAgain=Không hiển thị lại popup.reportError.log=Mở log file popup.reportError.gitHub=Báo cáo cho người theo dõi vấn đề GitHub -popup.reportError={0}\n\nĐể giúp chúng tôi cải tiến phần mềm, vui lòng báo cáo lỗi này bằng cách mở một thông báo vấn đề mới tại https://github.com/bisq-network/bisq/issues.\nTin nhắn lỗi phía trên sẽ được sao chép tới clipboard khi bạn ấn vào một nút bên dưới.\nSự cố sẽ được xử lý dễ dàng hơn nếu bạn đính kèm haveno.log file bằng cách nhấn "Mở log file", lưu bản sao, và đính kèm vào báo cáo lỗi. +popup.reportError={0}\n\nĐể giúp chúng tôi cải tiến phần mềm, vui lòng báo cáo lỗi này bằng cách mở một thông báo vấn đề mới tại https://github.com/haveno-dex/haveno/issues.\nTin nhắn lỗi phía trên sẽ được sao chép tới clipboard khi bạn ấn vào một nút bên dưới.\nSự cố sẽ được xử lý dễ dàng hơn nếu bạn đính kèm haveno.log file bằng cách nhấn "Mở log file", lưu bản sao, và đính kèm vào báo cáo lỗi. popup.error.tryRestart=Hãy khởi động lại ứng dụng và kiểm tra kết nối mạng để xem bạn có thể xử lý vấn đề này hay không. popup.error.takeOfferRequestFailed=Có lỗi xảy ra khi ai đó cố gắng để nhận một trong các chào giá của bạn:\n{0} @@ -1607,6 +1632,7 @@ popup.warning.nodeBanned=One of the {0} nodes got banned. popup.warning.priceRelay=rơle giá popup.warning.seed=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=Chúng tôi không nhận được đối tượng bộ lọc từ các nút hạt giống. Vui lòng thông báo cho quản trị viên mạng để đăng ký một đối tượng bộ lọc. popup.warning.burnXMR=Không thể thực hiện giao dịch, vì phí đào {0} vượt quá số lượng {1} cần chuyển. Vui lòng chờ tới khi phí đào thấp xuống hoặc khi bạn tích lũy đủ XMR để chuyển. 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. @@ -1674,6 +1700,9 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.buyerAsTakerWithoutDeposit.headline=Không cần đặt cọc từ người mua +popup.info.buyerAsTakerWithoutDeposit=Lời đề nghị của bạn sẽ không yêu cầu khoản đặt cọc bảo mật hoặc phí từ người mua XMR.\n\nĐể chấp nhận lời đề nghị của bạn, bạn phải chia sẻ một mật khẩu với đối tác giao dịch ngoài Haveno.\n\nMật khẩu được tạo tự động và hiển thị trong chi tiết lời đề nghị sau khi tạo. + #################################################################### # Notifications #################################################################### @@ -1799,11 +1828,14 @@ navigation.support=\"Hỗ trợ\" formatter.formatVolumeLabel={0} giá trị {1} formatter.makerTaker=Người tạo là {0} {1} / Người nhận là {2} {3} +formatter.makerTaker.locked=Người tạo là {0} {1} / Người nhận là {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=Bạn là {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=Bạn đang tạo một chào giá đến {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=Bạn đang tạo một chào giá đến {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=Bạn đang tạo một chào giá đến {0} {1} ({2} {3}) 🔒 formatter.asMaker={0} {1} như người tạo formatter.asTaker={0} {1} như người nhận @@ -1946,17 +1978,15 @@ 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.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. 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. # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info=Please be aware that all bank transfers carry a certain amount of chargeback risk. To mitigate this risk, Haveno sets per-trade limits based on the estimated level of chargeback risk for the payment method used.\n\nFor this payment method, your per-trade limit for buying and selling is {2}.\n\nThis limit only applies to the size of a single trade—you can place as many trades as you like.\n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://haveno.exchange/wiki/Account_limits]. +payment.limits.info.withSigning=To limit chargeback risk, Haveno sets per-trade limits for this payment account type based on the following 2 factors:\n\n1. General chargeback risk for the payment method\n2. Account signing status\n\nThis payment account is not yet signed, so it is limited to buying {0} per trade. After signing, buy limits will increase as follows:\n\n● Before signing, and for 30 days after signing, your per-trade buy limit will be {0}\n● 30 days after signing, your per-trade buy limit will be {1}\n● 60 days after signing, your per-trade buy limit will be {2}\n\nSell limits are not affected by account signing. You can sell {2} in a single trade immediately.\n\nThese limits only apply to the size of a single trade—you can place as many trades as you like. \n\nSee more details on the wiki [HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]. 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. @@ -1964,12 +1994,14 @@ payment.revolut.info=Revolut requires the 'Username' as account ID not the phone 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. +payment.venmo.info=Vui lòng lưu ý rằng Venmo 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. +payment.paypal.info=Vui lòng lưu ý rằng PayPal 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. + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account -payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Haveno requires that you understand the following:\n\n- XMR buyers must write the XMR Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- XMR buyers must send the USPMO to the XMR seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Haveno mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Haveno. - payment.payByMail.contact=thông tin liên hệ payment.payByMail.contact.prompt=Name or nym envelope should be addressed to payment.f2f.contact=thông tin liên hệ @@ -1978,11 +2010,13 @@ payment.f2f.city=Thành phố để gặp mặt trực tiếp payment.f2f.city.prompt=Thành phố sẽ được hiển thị cùng báo giá payment.shared.optionalExtra=Thông tin thêm tuỳ chọn. payment.shared.extraInfo=thông tin thêm -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). -payment.f2f.info='Face to Face' trades have different rules and come with different risks than online transactions.\n\nThe main differences are:\n● The trading peers need to exchange information about the meeting location and time by using their provided contact details.\n● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n● In case of a dispute the mediator or arbitrator cannot be of much assistance as it is usually difficult to get tamper-proof evidence of what happened at the meeting. In such cases the XMR funds might get locked indefinitely or until the trading peers come to an agreement.\n\nTo be sure you fully understand the differences with 'Face to Face' trades please read the instructions and recommendations at: [HYPERLINK:https://docs.haveno.exchange/trading-rules.html#f2f-trading] +payment.shared.extraInfo.offer=Thông tin bổ sung về ưu đãi +payment.shared.extraInfo.prompt.paymentAccount=Xác định bất kỳ điều khoản, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với các ưu đãi của mình cho tài khoản thanh toán này (người dùng sẽ thấy thông tin này trước khi chấp nhận các ưu đãi). +payment.shared.extraInfo.prompt.offer=Xác định bất kỳ thuật ngữ, điều kiện hoặc chi tiết đặc biệt nào bạn muốn hiển thị cùng với đề nghị của mình. +payment.shared.extraInfo.noDeposit=Chi tiết liên hệ và điều khoản ưu đãi payment.f2f.info.openURL=Mở trang web payment.f2f.offerbook.tooltip.countryAndCity=Country and city: {0} / {1} -payment.f2f.offerbook.tooltip.extra=Thông tin thêm: {0} +payment.shared.extraInfo.tooltip=Thông tin thêm: {0} payment.japan.bank=Ngân hàng payment.japan.branch=Branch @@ -1991,7 +2025,10 @@ 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://haveno.exchange/wiki/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. # We use constants from the code so we do not use our normal naming convention @@ -2071,7 +2108,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=Amazon eGift Card # suppress inspection "UnusedProperty" @@ -2123,7 +2160,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=Amazon eGift Card # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index bc2d39b144..f1b086eec3 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -40,6 +40,8 @@ shared.buyMonero=买入比特币 shared.sellMonero=卖出比特币 shared.buyCurrency=买入 {0} shared.sellCurrency=卖出 {0} +shared.buyCurrency.locked=买入 {0} 🔒 +shared.sellCurrency.locked=卖出 {0} 🔒 shared.buyingXMRWith=用 {0} 买入 XMR shared.sellingXMRFor=卖出 XMR 为 {0} shared.buyingCurrency=买入 {0}(卖出 XMR) @@ -103,7 +105,7 @@ shared.faq=访问 FAQ 页面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=选择交易账户 -shared.fundFromSavingsWalletButton=从 Haveno 钱包资金划转 +shared.fundFromSavingsWalletButton=从 Haveno 钱包申请资金 shared.fundFromExternalWalletButton=从您的外部钱包充值 shared.openDefaultWalletFailed=打开默认的比特币钱包应用程序失败了。您确定您安装了吗? shared.belowInPercent=低于市场价格 % @@ -123,6 +125,7 @@ shared.noDateAvailable=没有可用数据 shared.noDetailsAvailable=没有可用详细 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=TOD发送:{0}\n\n接收地址:{1}\n\n额外矿工费:{2}\n\n您确定要发送此金额吗?O # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 检测到,该交易将产生一个低于最低零头阈值的输出(不被比特币共识规则所允许)。相反,这些零头({0}satoshi{1})将被添加到挖矿手续费中。 shared.copyToClipboard=复制到剪贴板 @@ -139,6 +142,7 @@ shared.addNewAccount=添加新的账户 shared.ExportAccounts=导出账户 shared.importAccounts=导入账户 shared.createNewAccount=创建新的账户 +shared.createNewAccountDescription=您的账户详情存储在您的设备上,仅与您的交易对手和仲裁员在出现争议时共享。 shared.saveNewAccount=保存新的账户 shared.selectedAccount=选中的账户 shared.deleteAccount=删除账户 @@ -185,13 +189,14 @@ shared.total=合计 shared.totalsNeeded=需要资金 shared.tradeWalletAddress=交易钱包地址 shared.tradeWalletBalance=交易钱包余额 -shared.reserveExactAmount=仅保留所需的资金。在您的交易生效前可能需要支付挖矿费和10次确认(约20分钟)。 +shared.reserveExactAmount=仅保留必要的资金。需要支付矿工费用,并且大约需要 20 分钟后您的报价才会生效。 shared.makerTxFee=卖家:{0} shared.takerTxFee=买家:{0} shared.iConfirm=我确认 shared.openURL=打开 {0} shared.fiat=法定货币 shared.crypto=加密 +shared.preciousMetals=贵金属 shared.all=全部 shared.edit=编辑 shared.advancedOptions=高级选项 @@ -328,6 +333,7 @@ offerbook.createOffer=创建报价 offerbook.takeOffer=接受报价 offerbook.takeOfferToBuy=接受报价来收购 {0} offerbook.takeOfferToSell=接受报价来出售 {0} +offerbook.takeOffer.enterChallenge=输入报价密码 offerbook.trader=商人 offerbook.offerersBankId=卖家的银行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=卖家的银行名称:{0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=接受的银行所在国家(买家):\n offerbook.availableOffers=可用报价 offerbook.filterByCurrency=以货币筛选 offerbook.filterByPaymentMethod=以支付方式筛选 -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=匹配我的账户的报价 +offerbook.filterNoDeposit=无押金 +offerbook.noDepositOffers=无押金的报价(需要密码短语) offerbook.timeSinceSigning=账户信息 offerbook.timeSinceSigning.info=此账户已验证,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁员验证,并可以验证伙伴账户 @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=账户已被封禁 offerbook.timeSinceSigning.daysSinceSigning={0} 天 offerbook.timeSinceSigning.daysSinceSigning.long=自验证{0} offerbook.xmrAutoConf=是否开启自动确认 +offerbook.buyXmrWith=使用以下方式购买 XMR: +offerbook.sellXmrFor=出售 XMR 以换取: offerbook.timeSinceSigning.help=当您成功地完成与拥有已验证付款帐户的伙伴交易时,您的付款帐户已验证。\n{0} 天后,最初的 {1} 的限制解除以及你的账户可以验证其他人的付款账户。 offerbook.timeSinceSigning.notSigned=尚未验证 @@ -362,6 +372,7 @@ offerbook.nrOffers=报价数量:{0} offerbook.volume={0}(最小 - 最大) offerbook.deposit=XMR 保证金(%) offerbook.deposit.help=交易双方均已支付保证金确保这个交易正常进行。这会在交易完成时退还。 +offerbook.createNewOffer=創建報價給 {0} {1} offerbook.createOfferToBuy=创建新的报价来买入 {0} offerbook.createOfferToSell=创建新的报价来卖出 {0} @@ -453,7 +464,12 @@ createOffer.placeOfferButton=复审:报价挂单 {0} 比特币 createOffer.createOfferFundWalletInfo.headline=为您的报价充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n -createOffer.createOfferFundWalletInfo.msg=这个报价您需要 {0} 作为保证金。\n\n这些资金保留在您的本地钱包并会被冻结到多重验证保证金地址直到报价交易成功。\n\n总数量:{1}\n- 保证金:{2}\n- 挂单费:{3}\n- 矿工手续费:{4}\n\n您有两种选项可以充值您的交易:\n- 使用您的 Haveno 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 +createOffer.createOfferFundWalletInfo.msg=您需要为此报价存入 {0}。\n\n\ + 这些资金将保留在您的本地钱包中,并在有人接受您的报价后锁定到多签钱包中。\n\n\ + 金额是以下各项的总和:\n\ + {1}\ + - 您的保证金:{2}\n\ + - 交易费用:{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=提交报价发生错误:\n\n{0}\n\n没有资金从您钱包中扣除。\n请检查您的互联网连接或尝试重启应用程序。 @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=设置自己作为买家的保证金(%) createOffer.setDepositForBothTraders=设置双方的保证金比例(%) createOffer.securityDepositInfo=您的买家的保证金将会是 {0} createOffer.securityDepositInfoAsBuyer=您作为买家的保证金将会是 {0} -createOffer.minSecurityDepositUsed=已使用最低买家保证金 +createOffer.minSecurityDepositUsed=最低安全押金已使用 +createOffer.buyerAsTakerWithoutDeposit=无需买家支付押金(使用口令保护) +createOffer.myDeposit=我的安全押金 (%) +createOffer.myDepositInfo=您的保证金为 {0} #################################################################### @@ -499,6 +518,8 @@ takeOffer.fundsBox.networkFee=总共挖矿手续费 takeOffer.fundsBox.takeOfferSpinnerInfo=接受报价:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保证金,{1} 交易费,{2} 采矿费) +takeOffer.fundsBox.noFundingRequiredTitle=无需资金 +takeOffer.fundsBox.noFundingRequiredDescription=从卖方处获取交易密码(在Haveno之外)以接受此报价。 takeOffer.success.headline=你已成功下单一个报价。 takeOffer.success.info=你可以在“业务/未完成交易”页面内查看您的未完成交易。 takeOffer.error.message=下单时发生了一个错误。\n\n{0} @@ -509,7 +530,7 @@ takeOffer.noPriceFeedAvailable=您不能对这笔报价下单,因为它使用 takeOffer.takeOfferFundWalletInfo.headline=为交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易数量:{0}\n -takeOffer.takeOfferFundWalletInfo.msg=这个报价您需要付出 {0} 保证金。\n\n这些资金保留在您的本地钱包并会被冻结到多重验证保证金地址直到报价交易成功。\n\n总数量:{1}\n- 保证金:{2}\n- 挂单费:{3}\n\n您有两种选项可以充值您的交易:\n- 使用您的 Haveno 钱包(方便,但交易可能会被链接到)或者\n- 从外部钱包转入(或许这样更隐秘一些)\n\n关闭此弹出窗口后,您将看到所有资金选项和详细信息。 +takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 以接受此报价。\n\n该金额为以下总和:\n{1}- 您的保证金:{2}\n- 交易费用:{3} takeOffer.alreadyPaidInFunds=如果你已经支付,你可以在“资金/提现”提现它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=设置数量 @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=您请求的服务发生了错误。没 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服务返回失败。没有自动确认。 -portfolio.pending.step1.info=存款交易已经发布。\n开始付款之前,{0} 需要等待至少一个区块链确认。 +portfolio.pending.step1.info.you=存款交易已发布。\n您需要等待 10 次确认(约 20 分钟)后付款才能开始。 +portfolio.pending.step1.info.buyer=存款交易已发布。\nXMR 买家需要等待 10 次确认(大约 20 分钟),然后才能开始付款。 portfolio.pending.step1.warn=保证金交易仍未得到确认。这种情况可能会发生在外部钱包转账时使用的交易手续费用较低造成的。 portfolio.pending.step1.openForDispute=保证金交易仍未得到确认。请联系调解员协助。 @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=请用“美国邮政汇票”发送 {0} 给 XMR 卖家。\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -798,11 +820,11 @@ 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/bisq-network/support/issues\n\n请随意的将该交易移至失败交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/haveno-dex/haveno/issues\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 +portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易协议出现了问题。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易协议出现了严重问题。\n\n{0}\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=这个交易协议存在一些问题。\n\n{0}\n\n这个报价交易已经被发布以及资金已被锁定。只有在确定情况下将该交易移至失败交易。这可能会阻止解决问题的可用选项。\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 @@ -1033,6 +1055,8 @@ settings.net.p2pHeader=Haveno 网络 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义比特币主节点 settings.net.moneroPeersLabel=已连接节点 +settings.net.connection=连接 +settings.net.connected=连接 settings.net.useTorForXmrJLabel=使用 Tor 连接 Monero 网络 settings.net.moneroNodesLabel=需要连接 Monero settings.net.useProvidedNodesRadio=使用公共比特币核心节点 @@ -1453,6 +1477,7 @@ offerDetailsWindow.confirm.maker=确定:发布报价 {0} 比特币 offerDetailsWindow.confirm.taker=确定:下单买入 {0} 比特币 offerDetailsWindow.creationDate=创建时间 offerDetailsWindow.makersOnion=卖家的匿名地址 +offerDetailsWindow.challenge=提供密码 qRCodeWindow.headline=二维码 qRCodeWindow.msg=请使用二维码从外部钱包充值至 Haveno 钱包 @@ -1493,7 +1518,7 @@ tacWindow.disagree=我不同意并退出 tacWindow.arbitrationSystem=纠纷解决方案 tradeDetailsWindow.headline=交易 -tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID: +tradeDetailsWindow.disputedPayoutTxId=纠纷支付交易 ID tradeDetailsWindow.tradeDate=交易时间 tradeDetailsWindow.txFee=矿工手续费 tradeDetailsWindow.tradePeersOnion=交易伙伴匿名地址 @@ -1567,7 +1592,7 @@ popup.headline.error=错误 popup.doNotShowAgain=不要再显示 popup.reportError.log=打开日志文件 popup.reportError.gitHub=报告至 Github issue tracker -popup.reportError={0}\n\n为了帮助我们改进软件,请在 https://github.com/bisq-network/bisq/issues 上打开一个新问题来报告这个 bug 。\n\n当您单击下面任意一个按钮时,上面的错误消息将被复制到剪贴板。\n\n如果您通过按下“打开日志文件”,保存一份副本,并将其附加到 bug 报告中,如果包含 haveno.log 文件,那么调试就会变得更容易。 +popup.reportError={0}\n\n为了帮助我们改进软件,请在 https://github.com/haveno-dex/haveno/issues 上打开一个新问题来报告这个 bug 。\n\n当您单击下面任意一个按钮时,上面的错误消息将被复制到剪贴板。\n\n如果您通过按下“打开日志文件”,保存一份副本,并将其附加到 bug 报告中,如果包含 haveno.log 文件,那么调试就会变得更容易。 popup.error.tryRestart=请尝试重启您的应用程序或者检查您的网络连接。 popup.error.takeOfferRequestFailed=当有人试图接受你的报价时发生了一个错误:\n{0} @@ -1612,6 +1637,7 @@ popup.warning.nodeBanned=其中一个 {0} 节点已被禁用 popup.warning.priceRelay=价格传递 popup.warning.seed=种子 popup.warning.mandatoryUpdate.trading=请更新到最新的 Haveno 版本。强制更新禁止了旧版本进行交易。更多信息请访问 Haveno 论坛。 +popup.warning.noFilter=我们没有从种子节点接收到过滤器对象。请通知网络管理员注册一个过滤器对象。 popup.warning.burnXMR=这笔交易是无法实现,因为 {0} 的挖矿手续费用会超过 {1} 的转账金额。请等到挖矿手续费再次降低或您积累了更多的 XMR 来转账。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 为 {0} 的挂单费交易被比特币网络拒绝。\n交易 ID = {1}\n交易已被移至失败交易。\n请到“设置/网络信息”进行 SPV 重新同步。\n如需更多帮助,请联系 Haveno Keybase 团队的 Support 频道 @@ -1681,6 +1707,9 @@ popup.accountSigning.unsignedPubKeys.signed=公钥已被验证 popup.accountSigning.unsignedPubKeys.result.signed=已验证公钥 popup.accountSigning.unsignedPubKeys.result.failed=未能验证公钥 +popup.info.buyerAsTakerWithoutDeposit.headline=买家无需支付保证金 +popup.info.buyerAsTakerWithoutDeposit=您的报价将不需要来自XMR买家的保证金或费用。\n\n要接受您的报价,您必须与交易伙伴在Haveno外共享一个密码短语。\n\n密码短语会自动生成,并在创建后显示在报价详情中。 + #################################################################### # Notifications #################################################################### @@ -1806,11 +1835,14 @@ navigation.support=“帮助” formatter.formatVolumeLabel={0} 数量 {1} formatter.makerTaker=卖家 {0} {1} / 买家 {2} {3} +formatter.makerTaker.locked=卖家 {0} {1} / 买家 {2} {3} 🔒 formatter.youAreAsMaker=您是 {1} {0} 卖家 / 买家是 {3} {2} formatter.youAreAsTaker=您是 {1} {0} 买家 / 卖家是 {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您创建新的报价 {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=您创建新的报价 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正创建报价 {0} {1}({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=您正创建报价 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是卖家 formatter.asTaker={0} {1} 是买家 @@ -1953,17 +1985,15 @@ payment.accountType=账户类型 payment.checking=检查 payment.savings=保存 payment.personalId=个人 ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle是一项转账服务,转账到其他银行做的很好。\n\n1.检查此页面以查看您的银行是否(以及如何)与 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特别注意您的转账限额-汇款限额因银行而异,银行通常分别指定每日,每周和每月的限额。\n\n3.如果您的银行不能使用 Zelle,您仍然可以通过 Zelle 移动应用程序使用它,但是您的转账限额会低得多。\n\n4.您的 Haveno 帐户上指定的名称必须与 Zelle/银行帐户上的名称匹配。 \n\n如果您无法按照贸易合同中的规定完成 Zelle 交易,则可能会损失部分(或全部)保证金。\n\n由于 Zelle 的拒付风险较高,因此建议卖家通过电子邮件或 SMS 与未签名的买家联系,以确认买家确实拥有 Haveno 中指定的 Zelle 帐户。 payment.fasterPayments.newRequirements.info=有些银行已经开始核实快捷支付收款人的全名。您当前的快捷支付帐户没有填写全名。\n\n请考虑在 Haveno 中重新创建您的快捷支付帐户,为将来的 {0} 买家提供一个完整的姓名。\n\n重新创建帐户时,请确保将银行区号、帐户编号和帐龄验证盐值从旧帐户复制到新帐户。这将确保您现有的帐龄和签名状态得到保留。 payment.moneyGram.info=使用 MoneyGram 时,XMR 买方必须将授权号码和收据的照片通过电子邮件发送给 XMR 卖方。收据必须清楚地显示卖方的全名、国家或地区、州和金额。买方将在交易过程中显示卖方的电子邮件。 payment.westernUnion.info=使用 Western Union 时,XMR 买方必须通过电子邮件将 MTCN(运单号)和收据照片发送给 XMR 卖方。收据上必须清楚地显示卖方的全名、城市、国家或地区和金额。买方将在交易过程中显示卖方的电子邮件。 payment.halCash.info=使用 HalCash 时,XMR 买方需要通过手机短信向 XMR 卖方发送 HalCash 代码。\n\n请确保不要超过银行允许您用半现金汇款的最高金额。每次取款的最低金额是 10 欧元,最高金额是 10 欧元。金额是 600 欧元。对于重复取款,每天每个接收者 3000 欧元,每月每个接收者 6000 欧元。请与您的银行核对这些限额,以确保它们使用与此处所述相同的限额。\n\n提现金额必须是 10 欧元的倍数,因为您不能从 ATM 机提取其他金额。 创建报价和下单屏幕中的 UI 将调整 XMR 金额,使 EUR 金额正确。你不能使用基于市场的价格,因为欧元的数量会随着价格的变化而变化。\n # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Haveno 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://haveno.exchange/wiki/Account_limits]。 +payment.limits.info=请注意,所有银行转账都有一定的退款风险。为了降低这一风险,Haveno 基于使用的付款方式的退款风险。\n\n对于付款方式,您的每笔交易的出售和购买的限额为{2}\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://haveno.exchange/wiki/Account_limits +payment.limits.info.withSigning=为了降低这一风险,Haveno 基于两个因素对该付款方式每笔交易设置了限制:\n\n1. 使用的付款方法的预估退款风险水平\n2. 您的付款方式的账龄\n\n这个付款账户还没有被验证,所以他每个交易最多购买{0}。在验证之后,购买限制会以以下规则逐渐增加:\n\n●签署前,以及签署后30天内,您的每笔最大交易将限制为{0}\n●签署后30天,每笔最大交易将限制为{1}\n●签署后60天,每笔最大交易将限制为{2}\n\n出售限制不会被账户验证状态限制,你可以理科进行单笔为{2}的交易\n\n限制只应用在单笔交易,你可以尽可能多的进行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/the-project/account_limits payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 @@ -1971,6 +2001,10 @@ payment.revolut.info=Revolut 要求使用“用户名”作为帐户 ID,而不 payment.account.revolut.addUserNameInfo={0}\n您现有的 Revolut 帐户({1})尚未设置“用户名”。\n请输入您的 Revolut ``用户名''以更新您的帐户数据。\n这不会影响您的账龄验证状态。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 账户 +payment.cashapp.info=请注意,Cash App 的退款风险高于大多数银行转账。 +payment.venmo.info=请注意,Venmo 的退款风险高于大多数银行转账。 +payment.paypal.info=请注意,PayPal 的退款风险高于大多数银行转账。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account @@ -1985,11 +2019,14 @@ payment.f2f.city=“面对面”会议的城市 payment.f2f.city.prompt=城市将与报价一同显示 payment.shared.optionalExtra=可选的附加信息 payment.shared.extraInfo=附加信息 -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=附加报价信息 +payment.shared.extraInfo.prompt.paymentAccount=定义您希望在此支付账户的报价中显示的任何特殊术语、条件或细节(用户在接受报价之前将看到这些信息)。 +payment.shared.extraInfo.prompt.offer=定义您希望随您的报价一起显示的任何特殊条款、条件或详细信息。 +payment.shared.extraInfo.noDeposit=联系方式和优惠条款 payment.f2f.info=与网上交易相比,“面对面”交易有不同的规则,也有不同的风险。\n\n主要区别是:\n●交易伙伴需要使用他们提供的联系方式交换关于会面地点和时间的信息。\n●交易双方需要携带笔记本电脑,在会面地点确认“已发送付款”和“已收到付款”。\n●如果交易方有特殊的“条款和条件”,他们必须在账户的“附加信息”文本框中声明这些条款和条件。\n●在发生争议时,调解员或仲裁员不能提供太多帮助,因为通常很难获得有关会面上所发生情况的篡改证据。在这种情况下,XMR 资金可能会被无限期锁定,或者直到交易双方达成协议。\n\n为确保您完全理解“面对面”交易的不同之处,请阅读以下说明和建议:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打开网页 payment.f2f.offerbook.tooltip.countryAndCity=国家或地区及城市:{0} / {1} -payment.f2f.offerbook.tooltip.extra=附加信息:{0} +payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=银行 payment.japan.branch=分行 @@ -1998,7 +2035,10 @@ 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://haveno.exchange/wiki/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\ + 为确保交易安全并支持争议解决,请始终使用提供可验证记录的支付方式。 # We use constants from the code so we do not use our normal naming convention @@ -2078,7 +2118,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" @@ -2130,7 +2170,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亚马逊电子礼品卡 # suppress inspection "UnusedProperty" diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 39a9595119..94554bc12f 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -40,6 +40,8 @@ shared.buyMonero=買入比特幣 shared.sellMonero=賣出比特幣 shared.buyCurrency=買入 {0} shared.sellCurrency=賣出 {0} +shared.buyCurrency.locked=買入 {0} 🔒 +shared.sellCurrency.locked=賣出 {0} 🔒 shared.buyingXMRWith=用 {0} 買入 XMR shared.sellingXMRFor=賣出 XMR 為 {0} shared.buyingCurrency=買入 {0}(賣出 XMR) @@ -103,7 +105,7 @@ shared.faq=訪問 FAQ 頁面 shared.yesCancel=是的,取消 shared.nextStep=下一步 shared.selectTradingAccount=選擇交易賬户 -shared.fundFromSavingsWalletButton=從 Haveno 錢包資金劃轉 +shared.fundFromSavingsWalletButton=從 Haveno 錢包申請資金 shared.fundFromExternalWalletButton=從您的外部錢包充值 shared.openDefaultWalletFailed=打開默認的比特幣錢包應用程序失敗了。您確定您安裝了嗎? shared.belowInPercent=低於市場價格 % @@ -123,6 +125,7 @@ shared.noDateAvailable=沒有可用數據 shared.noDetailsAvailable=沒有可用詳細 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=發送中:{0}\n\n至接收地址:{1}\n\n額外礦工費:{2}\n\n您確定要發送此金額嗎? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 檢測到,該交易將產生一個低於最低零頭閾值的輸出(不被比特幣共識規則所允許)。相反,這些零頭({0}satoshi{1})將被添加到挖礦手續費中。 shared.copyToClipboard=複製到剪貼板 @@ -139,6 +142,7 @@ shared.addNewAccount=添加新的賬户 shared.ExportAccounts=導出賬户 shared.importAccounts=導入賬户 shared.createNewAccount=創建新的賬户 +shared.createNewAccountDescription=您的帳戶詳細資料儲存在您的裝置上,僅在開啟爭議時與您的交易夥伴和仲裁者分享。 shared.saveNewAccount=保存新的賬户 shared.selectedAccount=選中的賬户 shared.deleteAccount=刪除賬户 @@ -185,13 +189,14 @@ shared.total=合計 shared.totalsNeeded=需要資金 shared.tradeWalletAddress=交易錢包地址 shared.tradeWalletBalance=交易錢包餘額 -shared.reserveExactAmount=僅保留所需的資金。在您的交易生效前可能需要支付礦工費和 10 次確認(約20分鐘)。 +shared.reserveExactAmount=僅保留必要的資金。需支付礦工費,約 20 分鐘後您的報價才會上線。 shared.makerTxFee=賣家:{0} shared.takerTxFee=買家:{0} shared.iConfirm=我確認 shared.openURL=打開 {0} shared.fiat=法定貨幣 shared.crypto=加密 +shared.preciousMetals=貴金屬 shared.all=全部 shared.edit=編輯 shared.advancedOptions=高級選項 @@ -328,6 +333,7 @@ offerbook.createOffer=創建報價 offerbook.takeOffer=接受報價 offerbook.takeOfferToBuy=接受報價來收購 {0} offerbook.takeOfferToSell=接受報價來出售 {0} +offerbook.takeOffer.enterChallenge=輸入報價密碼 offerbook.trader=商人 offerbook.offerersBankId=賣家的銀行 ID(BIC/SWIFT):{0} offerbook.offerersBankName=賣家的銀行名稱:{0} @@ -337,7 +343,9 @@ offerbook.offerersAcceptedBankSeats=接受的銀行所在國家(買家):\n offerbook.availableOffers=可用報價 offerbook.filterByCurrency=以貨幣篩選 offerbook.filterByPaymentMethod=以支付方式篩選 -offerbook.matchingOffers=Offers matching my accounts +offerbook.matchingOffers=符合我的帳戶的報價 +offerbook.filterNoDeposit=無押金 +offerbook.noDepositOffers=無押金的報價(需要密碼短語) offerbook.timeSinceSigning=賬户信息 offerbook.timeSinceSigning.info=此賬户已驗證,{0} offerbook.timeSinceSigning.info.arbitrator=由仲裁員驗證,並可以驗證夥伴賬户 @@ -348,6 +356,8 @@ offerbook.timeSinceSigning.info.banned=賬户已被封禁 offerbook.timeSinceSigning.daysSinceSigning={0} 天 offerbook.timeSinceSigning.daysSinceSigning.long=自驗證{0} offerbook.xmrAutoConf=是否開啟自動確認 +offerbook.buyXmrWith=購買 XMR 使用: +offerbook.sellXmrFor=出售 XMR 以換取: offerbook.timeSinceSigning.help=當您成功地完成與擁有已驗證付款帳户的夥伴交易時,您的付款帳户已驗證。\n{0} 天后,最初的 {1} 的限制解除以及你的賬户可以驗證其他人的付款賬户。 offerbook.timeSinceSigning.notSigned=尚未驗證 @@ -362,6 +372,7 @@ offerbook.nrOffers=報價數量:{0} offerbook.volume={0}(最小 - 最大) offerbook.deposit=XMR 保證金(%) offerbook.deposit.help=交易雙方均已支付保證金確保這個交易正常進行。這會在交易完成時退還。 +offerbook.createNewOffer=創建報價給 {0} {1} offerbook.createOfferToBuy=創建新的報價來買入 {0} offerbook.createOfferToSell=創建新的報價來賣出 {0} @@ -453,7 +464,12 @@ createOffer.placeOfferButton=複審:報價掛單 {0} 比特幣 createOffer.createOfferFundWalletInfo.headline=為您的報價充值 # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n -createOffer.createOfferFundWalletInfo.msg=這個報價您需要 {0} 作為保證金。\n\n這些資金保留在您的本地錢包並會被凍結到多重驗證保證金地址直到報價交易成功。\n\n總數量:{1}\n- 保證金:{2}\n- 掛單費:{3}\n- 礦工手續費:{4}\n\n您有兩種選項可以充值您的交易:\n- 使用您的 Haveno 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 +createOffer.createOfferFundWalletInfo.msg=您需要為此報價存入 {0}。\n\n\ + 這些資金會保留在您的本地錢包中,並在有人接受您的報價後鎖定到多重簽名錢包中。\n\n\ + 金額總和為:\n\ + {1}\ + - 您的保證金:{2}\n\ + - 交易費:{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=提交報價發生錯誤:\n\n{0}\n\n沒有資金從您錢包中扣除。\n請檢查您的互聯網連接或嘗試重啟應用程序。 @@ -475,7 +491,10 @@ createOffer.setDepositAsBuyer=設置自己作為買家的保證金(%) createOffer.setDepositForBothTraders=設置雙方的保證金比例(%) createOffer.securityDepositInfo=您的買家的保證金將會是 {0} createOffer.securityDepositInfoAsBuyer=您作為買家的保證金將會是 {0} -createOffer.minSecurityDepositUsed=已使用最低買家保證金 +createOffer.minSecurityDepositUsed=最低保證金已使用 +createOffer.buyerAsTakerWithoutDeposit=買家無需支付保證金(通行密碼保護) +createOffer.myDeposit=我的保證金(%) +createOffer.myDepositInfo=您的保證金將為 {0} #################################################################### @@ -499,6 +518,8 @@ takeOffer.fundsBox.networkFee=總共挖礦手續費 takeOffer.fundsBox.takeOfferSpinnerInfo=接受報價:{0} takeOffer.fundsBox.paymentLabel=Haveno 交易 ID {0} takeOffer.fundsBox.fundsStructure=({0} 保證金,{1} 交易費,{2} 採礦費) +takeOffer.fundsBox.noFundingRequiredTitle=無需資金 +takeOffer.fundsBox.noFundingRequiredDescription=從賣家那裡在 Haveno 之外獲取優惠密碼以接受此優惠。 takeOffer.success.headline=你已成功下單一個報價。 takeOffer.success.info=你可以在“業務/未完成交易”頁面內查看您的未完成交易。 takeOffer.error.message=下單時發生了一個錯誤。\n\n{0} @@ -509,7 +530,7 @@ takeOffer.noPriceFeedAvailable=您不能對這筆報價下單,因為它使用 takeOffer.takeOfferFundWalletInfo.headline=為交易充值 # suppress inspection "TrailingSpacesInProperty" takeOffer.takeOfferFundWalletInfo.tradeAmount=- 交易數量:{0}\n -takeOffer.takeOfferFundWalletInfo.msg=這個報價您需要付出 {0} 保證金。\n\n這些資金保留在您的本地錢包並會被凍結到多重驗證保證金地址直到報價交易成功。\n\n總數量:{1}\n- 保證金:{2}\n- 掛單費:{3}\n\n您有兩種選項可以充值您的交易:\n- 使用您的 Haveno 錢包(方便,但交易可能會被鏈接到)或者\n- 從外部錢包轉入(或許這樣更隱祕一些)\n\n關閉此彈出窗口後,您將看到所有資金選項和詳細信息。 +takeOffer.takeOfferFundWalletInfo.msg=您需要存入 {0} 才能接受此報價。\n\n該金額是以下總和:\n{1}- 您的保證金:{2}\n- 交易費用:{3} takeOffer.alreadyPaidInFunds=如果你已經支付,你可以在“資金/提現”提現它。 takeOffer.paymentInfo=付款信息 takeOffer.setAmountPrice=設置數量 @@ -595,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=您請求的服務發生了錯誤。沒 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服務返回失敗。沒有自動確認。 -portfolio.pending.step1.info=存款交易已經發布。\n開始付款之前,{0} 需要等待至少一個區塊鏈確認。 +portfolio.pending.step1.info.you=存款交易已發布。\n您需要等待 10 次確認(約 20 分鐘)後,付款才能開始。 +portfolio.pending.step1.info.buyer=存款交易已發佈。\nXMR 購買者需要等待 10 次確認(大約 20 分鐘)才能開始付款。 portfolio.pending.step1.warn=保證金交易仍未得到確認。這種情況可能會發生在外部錢包轉賬時使用的交易手續費用較低造成的。 portfolio.pending.step1.openForDispute=保證金交易仍未得到確認。請聯繫調解員協助。 @@ -620,7 +642,7 @@ portfolio.pending.step2_buyer.westernUnion.extra=重要要求:\n完成支付 # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.postal=請用“美國郵政匯票”發送 {0} 給 XMR 賣家。\n\n # 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://haveno.exchange/wiki/Cash_by_Mail].\n\n +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 # suppress inspection "TrailingSpacesInProperty" @@ -798,11 +820,11 @@ 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/bisq-network/support/issues\n\n請隨意的將該交易移至失敗交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/haveno-dex/haveno/issues\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 +portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易協議出現了問題。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易協議出現了嚴重問題。\n\n{0}\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=這個交易協議存在一些問題。\n\n{0}\n\n這個報價交易已經被髮布以及資金已被鎖定。只有在確定情況下將該交易移至失敗交易。這可能會阻止解決問題的可用選項。\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 @@ -1033,6 +1055,8 @@ settings.net.p2pHeader=Haveno 網絡 settings.net.onionAddressLabel=我的匿名地址 settings.net.xmrNodesLabel=使用自定义Monero节点 settings.net.moneroPeersLabel=已連接節點 +settings.net.connection=連接 +settings.net.connected=連接完成 settings.net.useTorForXmrJLabel=使用 Tor 連接 Monero 網絡 settings.net.moneroNodesLabel=需要連接 Monero settings.net.useProvidedNodesRadio=使用公共比特幣核心節點 @@ -1453,6 +1477,7 @@ offerDetailsWindow.confirm.maker=確定:發佈報價 {0} 比特幣 offerDetailsWindow.confirm.taker=確定:下單買入 {0} 比特幣 offerDetailsWindow.creationDate=創建時間 offerDetailsWindow.makersOnion=賣家的匿名地址 +offerDetailsWindow.challenge=提供密碼 qRCodeWindow.headline=二維碼 qRCodeWindow.msg=請使用二維碼從外部錢包充值至 Haveno 錢包 @@ -1493,7 +1518,7 @@ tacWindow.disagree=我不同意並退出 tacWindow.arbitrationSystem=糾紛解決方案 tradeDetailsWindow.headline=交易 -tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID: +tradeDetailsWindow.disputedPayoutTxId=糾紛支付交易 ID tradeDetailsWindow.tradeDate=交易時間 tradeDetailsWindow.txFee=礦工手續費 tradeDetailsWindow.tradePeersOnion=交易夥伴匿名地址 @@ -1565,7 +1590,7 @@ popup.headline.error=錯誤 popup.doNotShowAgain=不要再顯示 popup.reportError.log=打開日誌文件 popup.reportError.gitHub=報吿至 Github issue tracker -popup.reportError={0}\n\n為了幫助我們改進軟件,請在 https://github.com/bisq-network/bisq/issues 上打開一個新問題來報吿這個 bug 。\n\n當您單擊下面任意一個按鈕時,上面的錯誤消息將被複制到剪貼板。\n\n如果您通過按下“打開日誌文件”,保存一份副本,並將其附加到 bug 報吿中,如果包含 haveno.log 文件,那麼調試就會變得更容易。 +popup.reportError={0}\n\n為了幫助我們改進軟件,請在 https://github.com/haveno-dex/haveno/issues 上打開一個新問題來報吿這個 bug 。\n\n當您單擊下面任意一個按鈕時,上面的錯誤消息將被複制到剪貼板。\n\n如果您通過按下“打開日誌文件”,保存一份副本,並將其附加到 bug 報吿中,如果包含 haveno.log 文件,那麼調試就會變得更容易。 popup.error.tryRestart=請嘗試重啟您的應用程序或者檢查您的網絡連接。 popup.error.takeOfferRequestFailed=當有人試圖接受你的報價時發生了一個錯誤:\n{0} @@ -1608,6 +1633,7 @@ popup.warning.nodeBanned=其中一個 {0} 節點已被禁用 popup.warning.priceRelay=價格傳遞 popup.warning.seed=種子 popup.warning.mandatoryUpdate.trading=請更新到最新的 Haveno 版本。強制更新禁止了舊版本進行交易。更多信息請訪問 Haveno 論壇。 +popup.warning.noFilter=我們未從種子節點收到過濾器物件。請通知網路管理員註冊過濾器物件。 popup.warning.burnXMR=這筆交易是無法實現,因為 {0} 的挖礦手續費用會超過 {1} 的轉賬金額。請等到挖礦手續費再次降低或您積累了更多的 XMR 來轉賬。 popup.warning.openOffer.makerFeeTxRejected=交易 ID 為 {0} 的掛單費交易被比特幣網絡拒絕。\n交易 ID = {1}\n交易已被移至失敗交易。\n請到“設置/網絡信息”進行 SPV 重新同步。\n如需更多幫助,請聯繫 Haveno Keybase 團隊的 Support 頻道 @@ -1675,6 +1701,9 @@ popup.accountSigning.unsignedPubKeys.signed=公鑰已被驗證 popup.accountSigning.unsignedPubKeys.result.signed=已驗證公鑰 popup.accountSigning.unsignedPubKeys.result.failed=未能驗證公鑰 +popup.info.buyerAsTakerWithoutDeposit.headline=買家無需支付保證金 +popup.info.buyerAsTakerWithoutDeposit=您的報價不需要來自XMR買家的保證金或費用。\n\n要接受您的報價,您必須與您的交易夥伴在Haveno之外分享密碼短語。\n\n密碼短語會自動生成並在報價創建後顯示在報價詳情中。 + #################################################################### # Notifications #################################################################### @@ -1800,11 +1829,14 @@ navigation.support=“幫助” formatter.formatVolumeLabel={0} 數量 {1} formatter.makerTaker=賣家 {0} {1} / 買家 {2} {3} +formatter.makerTaker.locked=賣家 {0} {1} / 買家 {2} {3} 🔒 formatter.youAreAsMaker=You are: {1} {0} (maker) / Taker is: {3} {2} formatter.youAreAsTaker=You are: {1} {0} (taker) / Maker is: {3} {2} formatter.youAre=您是 {0} {1} ({2} {3}) formatter.youAreCreatingAnOffer.traditional=您創建新的報價 {0} {1} +formatter.youAreCreatingAnOffer.traditional.locked=您創建新的報價 {0} {1} 🔒 formatter.youAreCreatingAnOffer.crypto=您正創建報價 {0} {1}({2} {3}) +formatter.youAreCreatingAnOffer.crypto.locked=您正創建報價 {0} {1}({2} {3}) 🔒 formatter.asMaker={0} {1} 是賣家 formatter.asTaker={0} {1} 是買家 @@ -1947,17 +1979,15 @@ payment.accountType=賬户類型 payment.checking=檢查 payment.savings=保存 payment.personalId=個人 ID -payment.makeOfferToUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- make offers >{0}, so you only deal with signed/trusted buyers\n- keep any offers to sell <{0} to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. -payment.takeOfferFromUnsignedAccount.warning=With the recent rise in XMR price, beware that selling {0} or less incurs higher risk than before.\n\nIt is highly recommended to either:\n- take offers from signed buyers only\n- keep trades with unsigned/untrusted buyers to around ~100 USD in value, as this value has (historically) discouraged scammers\n\nHaveno developers are working on better ways to secure the payment account model for such smaller trades. Join the discussion here: [HYPERLINK:https://github.com/bisq-network/bisq/discussions/5339]. payment.zelle.info=Zelle是一項轉賬服務,轉賬到其他銀行做的很好。\n\n1.檢查此頁面以查看您的銀行是否(以及如何)與 Zelle 合作:\nhttps://www.zellepay.com/get-started\n\n2.特別注意您的轉賬限額-匯款限額因銀行而異,銀行通常分別指定每日,每週和每月的限額。\n\n3.如果您的銀行不能使用 Zelle,您仍然可以通過 Zelle 移動應用程序使用它,但是您的轉賬限額會低得多。\n\n4.您的 Haveno 帳户上指定的名稱必須與 Zelle/銀行帳户上的名稱匹配。 \n\n如果您無法按照貿易合同中的規定完成 Zelle 交易,則可能會損失部分(或全部)保證金。\n\n由於 Zelle 的拒付風險較高,因此建議賣家通過電子郵件或 SMS 與未簽名的買家聯繫,以確認買家確實擁有 Haveno 中指定的 Zelle 帳户。 payment.fasterPayments.newRequirements.info=有些銀行已經開始核實快捷支付收款人的全名。您當前的快捷支付帳户沒有填寫全名。\n\n請考慮在 Haveno 中重新創建您的快捷支付帳户,為將來的 {0} 買家提供一個完整的姓名。\n\n重新創建帳户時,請確保將銀行區號、帳户編號和帳齡驗證鹽值從舊帳户複製到新帳户。這將確保您現有的帳齡和簽名狀態得到保留。 payment.moneyGram.info=使用 MoneyGram 時,XMR 買方必須將授權號碼和收據的照片通過電子郵件發送給 XMR 賣方。收據必須清楚地顯示賣方的全名、國家或地區、州和金額。買方將在交易過程中顯示賣方的電子郵件。 payment.westernUnion.info=使用 Western Union 時,XMR 買方必須通過電子郵件將 MTCN(運單號)和收據照片發送給 XMR 賣方。收據上必須清楚地顯示賣方的全名、城市、國家或地區和金額。買方將在交易過程中顯示賣方的電子郵件。 payment.halCash.info=使用 HalCash 時,XMR 買方需要通過手機短信向 XMR 賣方發送 HalCash 代碼。\n\n請確保不要超過銀行允許您用半現金匯款的最高金額。每次取款的最低金額是 10 歐元,最高金額是 10 歐元。金額是 600 歐元。對於重複取款,每天每個接收者 3000 歐元,每月每個接收者 6000 歐元。請與您的銀行核對這些限額,以確保它們使用與此處所述相同的限額。\n\n提現金額必須是 10 歐元的倍數,因為您不能從 ATM 機提取其他金額。 創建報價和下單屏幕中的 UI 將調整 XMR 金額,使 EUR 金額正確。你不能使用基於市場的價格,因為歐元的數量會隨着價格的變化而變化。\n # suppress inspection "UnusedMessageFormatParameter" -payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Haveno 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://haveno.exchange/wiki/Account_limits]。 +payment.limits.info=請注意,所有銀行轉賬都有一定的退款風險。為了降低這一風險,Haveno 基於使用的付款方式的退款風險。\n\n對於付款方式,您的每筆交易的出售和購買的限額為{2}\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 查看更多信息[HYPERLINK:https://docs.haveno.exchange/the-project/account_limits]。 # suppress inspection "UnusedProperty" -payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://haveno.exchange/wiki/Account_limits +payment.limits.info.withSigning=為了降低這一風險,Haveno 基於兩個因素對該付款方式每筆交易設置了限制:\n\n1. 使用的付款方法的預估退款風險水平\n2. 您的付款方式的賬齡\n\n這個付款賬户還沒有被驗證,所以他每個交易最多購買{0}。在驗證之後,購買限制會以以下規則逐漸增加:\n\n●簽署前,以及簽署後30天內,您的每筆最大交易將限制為{0}\n●簽署後30天,每筆最大交易將限制為{1}\n●簽署後60天,每筆最大交易將限制為{2}\n\n出售限制不會被賬户驗證狀態限制,你可以理科進行單筆為{2}的交易\n\n限制只應用在單筆交易,你可以儘可能多的進行交易。\n\n在 Haveno Wiki 上查看更多:\nhttps://docs.haveno.exchange/the-project/account_limits payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬户。例如,美國銀行和富國銀行不再允許此類存款。 @@ -1965,6 +1995,10 @@ payment.revolut.info=Revolut 要求使用“用户名”作為帳户 ID,而不 payment.account.revolut.addUserNameInfo={0}\n您現有的 Revolut 帳户({1})尚未設置“用户名”。\n請輸入您的 Revolut ``用户名''以更新您的帳户數據。\n這不會影響您的賬齡驗證狀態。 payment.revolut.addUserNameInfo.headLine=更新 Revolut 賬户 +payment.cashapp.info=請注意,Cash App 的退款風險高於大多數銀行轉帳。 +payment.venmo.info=請注意,Venmo 的退款風險高於大多數銀行轉帳。 +payment.paypal.info=請注意,PayPal 的退款風險高於大多數銀行轉帳。 + payment.amazonGiftCard.upgrade=Amazon gift cards payment method requires the country to be specified. payment.account.amazonGiftCard.addCountryInfo={0}\nYour existing Amazon Gift Card account ({1}) does not have a Country specified.\nPlease enter your Amazon Gift Card Country to update your account data.\nThis will not affect your account age status. payment.amazonGiftCard.upgrade.headLine=Update Amazon Gift Card account @@ -1979,11 +2013,14 @@ payment.f2f.city=“面對面”會議的城市 payment.f2f.city.prompt=城市將與報價一同顯示 payment.shared.optionalExtra=可選的附加信息 payment.shared.extraInfo=附加信息 -payment.shared.extraInfo.prompt=Define any special terms, conditions, or details you would like to be displayed with your offers for this payment account (users will see this info before accepting offers). +payment.shared.extraInfo.offer=額外的優惠資訊 +payment.shared.extraInfo.prompt.paymentAccount=定義您希望在此付款帳戶的報價中顯示的任何特殊術語、條件或細節(用戶在接受報價之前將看到這些資訊)。 +payment.shared.extraInfo.prompt.offer=定義您希望在您的報價中顯示的任何特殊條款、條件或詳細資訊。 +payment.shared.extraInfo.noDeposit=聯絡詳情及優惠條款 payment.f2f.info=與網上交易相比,“面對面”交易有不同的規則,也有不同的風險。\n\n主要區別是:\n●交易夥伴需要使用他們提供的聯繫方式交換關於會面地點和時間的信息。\n●交易雙方需要攜帶筆記本電腦,在會面地點確認“已發送付款”和“已收到付款”。\n●如果交易方有特殊的“條款和條件”,他們必須在賬户的“附加信息”文本框中聲明這些條款和條件。\n●在發生爭議時,調解員或仲裁員不能提供太多幫助,因為通常很難獲得有關會面上所發生情況的篡改證據。在這種情況下,XMR 資金可能會被無限期鎖定,或者直到交易雙方達成協議。\n\n為確保您完全理解“面對面”交易的不同之處,請閲讀以下説明和建議:“https://docs.haveno.exchange/trading-rules.html#f2f-trading” payment.f2f.info.openURL=打開網頁 payment.f2f.offerbook.tooltip.countryAndCity=國家或地區及城市:{0} / {1} -payment.f2f.offerbook.tooltip.extra=附加信息:{0} +payment.shared.extraInfo.tooltip=附加信息:{0} payment.japan.bank=銀行 payment.japan.branch=分行 @@ -1992,7 +2029,10 @@ 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://haveno.exchange/wiki/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\ + 為確保交易安全並支持爭議解決,請始終使用可驗證記錄的付款方式。 # We use constants from the code so we do not use our normal naming convention @@ -2072,7 +2112,7 @@ PROMPT_PAY=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE=TransferWise +TRANSFERWISE=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" @@ -2124,7 +2164,7 @@ PROMPT_PAY_SHORT=PromptPay # suppress inspection "UnusedProperty" ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" -TRANSFERWISE_SHORT=TransferWise +TRANSFERWISE_SHORT=Wise # suppress inspection "UnusedProperty" AMAZON_GIFT_CARD_SHORT=亞馬遜電子禮品卡 # suppress inspection "UnusedProperty" diff --git a/core/src/test/java/haveno/core/offer/OfferMaker.java b/core/src/test/java/haveno/core/offer/OfferMaker.java index 52084f209b..2f839e1a38 100644 --- a/core/src/test/java/haveno/core/offer/OfferMaker.java +++ b/core/src/test/java/haveno/core/offer/OfferMaker.java @@ -73,7 +73,8 @@ public class OfferMaker { 0, null, null, - null)); + null, + "My extra info")); public static final Maker btcUsdOffer = a(Offer); } diff --git a/core/src/test/java/haveno/core/user/PreferencesTest.java b/core/src/test/java/haveno/core/user/PreferencesTest.java index 365d54732b..0639ba11af 100644 --- a/core/src/test/java/haveno/core/user/PreferencesTest.java +++ b/core/src/test/java/haveno/core/user/PreferencesTest.java @@ -24,6 +24,7 @@ import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; +import haveno.core.xmr.nodes.XmrNodes; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import javafx.collections.ObservableList; @@ -45,6 +46,7 @@ public class PreferencesTest { private Preferences preferences; private PersistenceManager persistenceManager; + private XmrNodes xmrNodes; @BeforeEach public void setUp() { @@ -53,12 +55,12 @@ public class PreferencesTest { GlobalSettings.setLocale(en_US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); - persistenceManager = mock(PersistenceManager.class); Config config = new Config(); - XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences); preferences = new Preferences( persistenceManager, config, null, null); + xmrNodes = new XmrNodes(); + XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences, xmrNodes); } @Test diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 04b294e451..1c6b8b8de9 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -150,10 +150,14 @@ class GrpcOffersService extends OffersImplBase { req.getMarketPriceMarginPct(), req.getAmount(), req.getMinAmount(), - req.getBuyerSecurityDepositPct(), + req.getSecurityDepositPct(), req.getTriggerPrice(), req.getReserveExactAmount(), req.getPaymentAccountId(), + req.getIsPrivateOffer(), + req.getBuyerAsTakerWithoutDeposit(), + req.getExtraInfo(), + req.getSourceOfferId(), offer -> { // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. @@ -200,12 +204,12 @@ class GrpcOffersService extends OffersImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 1, SECONDS)); - put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 123078b246..2d650dfb53 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -138,6 +138,7 @@ class GrpcTradesService extends TradesImplBase { coreApi.takeOffer(req.getOfferId(), req.getPaymentAccountId(), req.getAmount(), + req.getChallenge(), trade -> { TradeInfo tradeInfo = toTradeInfo(trade); var reply = TakeOfferReply.newBuilder() @@ -250,15 +251,15 @@ class GrpcTradesService extends TradesImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); - put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java index a03dc5a73e..f0fcdcc984 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java @@ -47,8 +47,8 @@ import haveno.proto.grpc.CheckConnectionsReply; import haveno.proto.grpc.CheckConnectionsRequest; import haveno.proto.grpc.GetAutoSwitchReply; import haveno.proto.grpc.GetAutoSwitchRequest; -import haveno.proto.grpc.GetBestAvailableConnectionReply; -import haveno.proto.grpc.GetBestAvailableConnectionRequest; +import haveno.proto.grpc.GetBestConnectionReply; +import haveno.proto.grpc.GetBestConnectionRequest; import haveno.proto.grpc.GetConnectionReply; import haveno.proto.grpc.GetConnectionRequest; import haveno.proto.grpc.GetConnectionsReply; @@ -68,7 +68,7 @@ import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase; import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getCheckConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getCheckConnectionsMethod; -import static haveno.proto.grpc.XmrConnectionsGrpc.getGetBestAvailableConnectionMethod; +import static haveno.proto.grpc.XmrConnectionsGrpc.getGetBestConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod; @@ -201,12 +201,12 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { } @Override - public void getBestAvailableConnection(GetBestAvailableConnectionRequest request, - StreamObserver responseObserver) { + public void getBestConnection(GetBestConnectionRequest request, + StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection(); + MoneroRpcConnection connection = coreApi.getBestXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); - GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder(); + GetBestConnectionReply.Builder builder = GetBestConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); } @@ -314,7 +314,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); - put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); + put(getGetBestConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); }} ))); diff --git a/daemon/src/main/resources/logback.xml b/daemon/src/main/resources/logback.xml index e6ba6a6327..f838048ece 100644 --- a/daemon/src/main/resources/logback.xml +++ b/daemon/src/main/resources/logback.xml @@ -1,8 +1,11 @@ + + + - %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) diff --git a/desktop/package/linux/Haveno.AppDir/haveno.svg b/desktop/package/linux/Haveno.AppDir/exchange.haveno.Haveno.svg similarity index 100% rename from desktop/package/linux/Haveno.AppDir/haveno.svg rename to desktop/package/linux/Haveno.AppDir/exchange.haveno.Haveno.svg diff --git a/desktop/package/linux/Haveno.desktop b/desktop/package/linux/Haveno.desktop index a0e32a6347..b6d62c222c 100644 --- a/desktop/package/linux/Haveno.desktop +++ b/desktop/package/linux/Haveno.desktop @@ -3,7 +3,7 @@ Comment=A decentralized, Tor-based, P2P Monero exchange network. Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; bin/Haveno %u" GenericName[en_US]=Monero Exchange GenericName=Monero Exchange -Icon=haveno +Icon=exchange.haveno.Haveno Categories=Office;Finance;Java;P2P; Name[en_US]=Haveno Name=Haveno diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index 405d8aa657..57aeb02f09 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -15,7 +15,6 @@ monero - CC-BY-4.0 AGPL-3.0-only @@ -40,7 +39,6 @@

  • There is No token, because we don't need it. Transactions between traders are secured by non-custodial multisignature transactions on the Monero network.
  • - @@ -61,6 +59,7 @@ intense - - Haveno.desktop + + + diff --git a/desktop/package/linux/exchange.haveno.Haveno.yml b/desktop/package/linux/exchange.haveno.Haveno.yml index 2924684435..50843f8f75 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.yml +++ b/desktop/package/linux/exchange.haveno.Haveno.yml @@ -35,7 +35,7 @@ modules: - mkdir -p /app/share/icons/hicolor/128x128/apps/ - mkdir -p /app/share/applications/ - mkdir -p /app/share/metainfo/ - - mv icon.png /app/share/icons/hicolor/128x128/apps/haveno.png + - mv icon.png /app/share/icons/hicolor/128x128/apps/exchange.haveno.Haveno.png - mv Haveno.desktop /app/share/applications/exchange.haveno.Haveno.desktop - mv exchange.haveno.Haveno.metainfo.xml /app/share/metainfo/ diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 138ca4f704..c6bf09f2de 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.12 + 1.1.1 CFBundleShortVersionString - 1.0.12 + 1.1.1 CFBundleExecutable Haveno diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index 3e41aa2f8c..16370c2cdb 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -366,7 +366,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { } // check for open offers - if (injector.getInstance(OpenOfferManager.class).hasOpenOffers()) { + if (injector.getInstance(OpenOfferManager.class).hasAvailableOpenOffers()) { String key = "showOpenOfferWarnPopupAtShutDown"; if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { new Popup().warning(Res.get("popup.info.shutDownWithOpenOffers")) diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java index 353895e624..ed7e956eba 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoAppMain.java @@ -29,6 +29,7 @@ import haveno.desktop.setup.DesktopPersistedDataHost; import haveno.desktop.util.ImageUtil; import javafx.application.Application; import javafx.application.Platform; +import javafx.geometry.Pos; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; @@ -38,6 +39,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; +import javafx.stage.Window; import lombok.extern.slf4j.Slf4j; import java.util.Optional; @@ -199,7 +201,7 @@ public class HavenoAppMain extends HavenoExecutable { // Add an icon to the dialog Stage stage = (Stage) getDialogPane().getScene().getWindow(); - stage.getIcons().add(ImageUtil.getImageByPath("lock.png")); + stage.getIcons().add(ImageUtil.getImageByPath("lock@2x.png")); // Create the password field PasswordField passwordField = new PasswordField(); @@ -209,9 +211,13 @@ public class HavenoAppMain extends HavenoExecutable { Label errorMessageField = new Label(errorMessage); errorMessageField.setTextFill(Color.color(1, 0, 0)); + // Create the version field + Label versionField = new Label("v" + Version.VERSION); + // Set the dialog content VBox vbox = new VBox(10); - vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField); + vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField, versionField); + vbox.setAlignment(Pos.TOP_CENTER); getDialogPane().setContent(vbox); // Add OK and Cancel buttons @@ -228,6 +234,17 @@ public class HavenoAppMain extends HavenoExecutable { return null; } }); + + // Focus the password field when dialog is shown + Window window = getDialogPane().getScene().getWindow(); + if (window instanceof Stage) { + Stage dialogStage = (Stage) window; + dialogStage.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + passwordField.requestFocus(); + } + }); + } } } } diff --git a/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java b/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java index 501efe9a0d..8ae0b607f0 100644 --- a/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java +++ b/desktop/src/main/java/haveno/desktop/components/AccountStatusTooltipLabel.java @@ -106,7 +106,7 @@ public class AccountStatusTooltipLabel extends AutoTooltipLabel { learnMoreLink.setWrapText(true); learnMoreLink.setPadding(new Insets(10, 10, 2, 10)); learnMoreLink.getStyleClass().addAll("very-small-text"); - learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://haveno.exchange/wiki/Account_limits")); + learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://docs.haveno.exchange/the-project/account_limits")); VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink); vBox.setPadding(new Insets(2, 0, 2, 0)); diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextArea.java b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java new file mode 100644 index 0000000000..7bcd18de93 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java @@ -0,0 +1,140 @@ +/* + * 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.desktop.components; + + +import com.jfoenix.controls.JFXTextArea; +import haveno.core.util.validation.InputValidator; +import haveno.desktop.util.validation.JFXInputValidator; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Skin; + +/** + * TextArea with validation support. + * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is + * needed the validationResultProperty can be used for applying validation result done by external validation. + * In case the isValid property in validationResultProperty get set to false we display a red border and an error + * message within the errorMessageDisplay placed on the right of the text area. + * The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when + * hideErrorMessageDisplay() is called. + * There can be only 1 errorMessageDisplays at a time we use static field for it. + * The position is derived from the position of the textArea itself or if set from the layoutReference node. + */ +//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. +public class InputTextArea extends JFXTextArea { + + private final ObjectProperty validationResult = new SimpleObjectProperty<> + (new InputValidator.ValidationResult(true)); + + private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); + + private InputValidator validator; + private String errorMessage = null; + + + public InputValidator getValidator() { + return validator; + } + + public void setValidator(InputValidator validator) { + this.validator = validator; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InputTextArea() { + super(); + + getValidators().add(jfxValidationWrapper); + + validationResult.addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + jfxValidationWrapper.resetValidation(); + if (!newValue.isValid) { + if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking + validate(); // ensure that the new error message replaces the old one + } + if (this.errorMessage != null) { + jfxValidationWrapper.applyErrorMessage(this.errorMessage); + } else { + jfxValidationWrapper.applyErrorMessage(newValue); + } + } + validate(); + } + }); + + textProperty().addListener((o, oldValue, newValue) -> { + refreshValidation(); + }); + + focusedProperty().addListener((o, oldValue, newValue) -> { + if (validator != null) { + if (!oldValue && newValue) { + this.validationResult.set(new InputValidator.ValidationResult(true)); + } else { + this.validationResult.set(validator.validate(getText())); + } + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + public void resetValidation() { + jfxValidationWrapper.resetValidation(); + + String input = getText(); + if (input.isEmpty()) { + validationResult.set(new InputValidator.ValidationResult(true)); + } else { + validationResult.set(validator.validate(input)); + } + } + + public void refreshValidation() { + if (validator != null) { + this.validationResult.set(validator.validate(getText())); + } + } + + public void setInvalid(String message) { + validationResult.set(new InputValidator.ValidationResult(false, message)); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObjectProperty validationResultProperty() { + return validationResult; + } + + protected Skin createDefaultSkin() { + return new JFXTextAreaSkinHavenoStyle(this); + } +} diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java index f4d7cce695..6688bb591b 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AliPayForm.java @@ -26,6 +26,7 @@ import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.payment.validation.AliPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; +import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; @@ -44,6 +45,12 @@ public class AliPayForm extends GeneralAccountNumberForm { this.aliPayAccount = (AliPayAccount) paymentAccount; } + @Override + public void addTradeCurrency() { + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(aliPayAccount.getSupportedCurrencies())); + } + @Override void setAccountNumber(String newValue) { aliPayAccount.setAccountNr(newValue); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java index 234b5e88b0..615663bbbf 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -91,7 +91,7 @@ public class AustraliaPayidForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index 60fe712709..e1376c2643 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -79,7 +79,7 @@ public class CashAppForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { @@ -115,6 +115,8 @@ public class CashAppForm extends PaymentMethodForm { public void addFormForEditAccount() { gridRowFrom = gridRow; addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(cashAppAccount.getPaymentMethod().getId())); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), cashAppAccount.getEmailOrMobileNrOrCashtag()).second; diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java index 339dce099e..b6447a868d 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAtAtmForm.java @@ -41,12 +41,12 @@ public class CashAtAtmForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - CashAtAtmAccountPayload cbm = (CashAtAtmAccountPayload) paymentAccountPayload; + CashAtAtmAccountPayload cashAtAtmPayload = (CashAtAtmAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, 0, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); - textExtraInfo.setText(cbm.getExtraInfo()); + textExtraInfo.setText(cashAtAtmPayload.getExtraInfo()); return gridRow; } @@ -79,7 +79,11 @@ public class CashAtAtmForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(cashAtAtmAccount.getExtraInfo().substring(0, Math.min(50, cashAtAtmAccount.getExtraInfo().length()))); + if (cashAtAtmAccount.getExtraInfo() != null && !cashAtAtmAccount.getExtraInfo().isEmpty()) { + setAccountNameWithString(cashAtAtmAccount.getExtraInfo().substring(0, Math.min(50, cashAtAtmAccount.getExtraInfo().length()))); + } else { + setAccountNameWithString(cashAtAtmAccount.getSelectedTradeCurrency().getCode()); + } } @Override @@ -104,7 +108,6 @@ public class CashAtAtmForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && !cashAtAtmAccount.getExtraInfo().isEmpty() && paymentAccount.getSingleTradeCurrency() != null); } } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java index ada24a71d4..2a7e7f4f2a 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/F2FForm.java @@ -52,20 +52,20 @@ public class F2FForm extends PaymentMethodForm { private final F2FValidator f2fValidator; private Country selectedCountry; - public static int addFormForBuyer(GridPane gridPane, int gridRow, - PaymentAccountPayload paymentAccountPayload, Offer offer, double top) { + public static int addStep2Form(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload, Offer offer, double top, boolean isBuyer) { F2FAccountPayload f2fAccountPayload = (F2FAccountPayload) paymentAccountPayload; addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("shared.country"), CountryUtil.getNameAndCode(f2fAccountPayload.getCountryCode()), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.f2f.city"), offer.getF2FCity(), top); addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.f2f.contact"), - f2fAccountPayload.getContact()); + isBuyer ? f2fAccountPayload.getContact() : Res.get("shared.na")); TextArea textArea = addTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; textArea.setMinHeight(70); textArea.setEditable(false); textArea.setId("text-area-disabled"); - textArea.setText(offer.getExtraInfo()); + textArea.setText(offer.getPaymentAccountExtraInfo()); return gridRow; } @@ -106,7 +106,7 @@ public class F2FForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); //extraTextArea.setValidator(f2fValidator); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java index 8e0d48ee6e..83e4614a97 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java @@ -79,7 +79,7 @@ public class PayPalForm extends PaymentMethodForm { }); TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, - Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt.paymentAccount")).second; extraTextArea.setMinHeight(70); ((JFXTextArea) extraTextArea).setLabelFloat(false); extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java index 64d0066d5e..5abdee0d61 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaymentMethodForm.java @@ -184,14 +184,14 @@ public abstract class PaymentMethodForm { Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true)) + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true)) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.BUY, false), true), HavenoUtils.formatXmr(accountAgeWitnessService.getMyTradeLimit( - paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL), true), + paymentAccount, tradeCurrency.getCode(), OfferDirection.SELL, false), true), DisplayUtils.formatAccountAge(accountAge)); return limitationsText; } diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java new file mode 100644 index 0000000000..3eba4c78fa --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PaysafeForm.java @@ -0,0 +1,111 @@ +/* + * 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.desktop.components.paymentmethods; + +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.locale.Res; +import haveno.core.payment.PaymentAccount; +import haveno.core.payment.PaysafeAccount; +import haveno.core.payment.payload.PaysafeAccountPayload; +import haveno.core.payment.payload.PaymentAccountPayload; +import haveno.core.payment.validation.EmailValidator; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.util.validation.InputValidator; +import haveno.desktop.components.InputTextField; +import haveno.desktop.util.FormBuilder; +import javafx.scene.control.TextField; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; + +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; + +public class PaysafeForm extends PaymentMethodForm { + private final PaysafeAccount account; + private final EmailValidator validator = new EmailValidator(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, + PaymentAccountPayload paymentAccountPayload) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"), + ((PaysafeAccountPayload) paymentAccountPayload).getEmail()); + return gridRow; + } + + public PaysafeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, + InputValidator inputValidator, GridPane gridPane, + int gridRow, CoinFormatter formatter) { + super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); + this.account = (PaysafeAccount) paymentAccount; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email")); + emailInputTextField.setValidator(validator); + emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setEmail(newValue.trim()); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + private void addCurrenciesGrid(boolean isEditable) { + FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow, + Res.get("payment.supportedCurrenciesForReceiver"), 20, 20).second; + + if (isEditable) { + flowPane.setId("flow-pane-checkboxes-bg"); + } else { + flowPane.setId("flow-pane-checkboxes-non-editable-bg"); + } + + paymentAccount.getSupportedCurrencies().forEach(currency -> + fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account)); + } + + @Override + protected void autoFillNameTextField() { + setAccountNameWithString(account.getEmail()); + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(account.getPaymentMethod().getId())); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"), + account.getEmail()).second; + field.setMouseTransparent(false); + addLimitations(true); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && account.getEmail() != null + && validator.validate(account.getEmail()).isValid + && account.getTradeCurrencies().size() > 0); + } +} diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java index eb384687f2..cb1b40f6b7 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/WeChatPayForm.java @@ -26,6 +26,7 @@ import haveno.core.payment.payload.WeChatPayAccountPayload; import haveno.core.payment.validation.WeChatPayValidator; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.InputValidator; +import javafx.collections.FXCollections; import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; @@ -44,6 +45,12 @@ public class WeChatPayForm extends GeneralAccountNumberForm { this.weChatPayAccount = (WeChatPayAccount) paymentAccount; } + @Override + public void addTradeCurrency() { + addTradeCurrencyComboBox(); + currencyComboBox.setItems(FXCollections.observableArrayList(weChatPayAccount.getSupportedCurrencies())); + } + @Override void setAccountNumber(String newValue) { weChatPayAccount.setAccountNr(newValue); diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index 125b4c7bd9..e3cfac8c0e 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -501,15 +501,15 @@ tree-table-view:focused { -jfx-default-color: -bs-color-primary; } -.jfx-date-picker .jfx-text-field { +.jfx-date-picker .jfx-text-field .jfx-text-area { -fx-padding: 0.333333em 0em 0.333333em 0em; } -.jfx-date-picker .jfx-text-field > .input-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; } -.jfx-date-picker .jfx-text-field > .input-focused-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; } @@ -822,6 +822,10 @@ tree-table-view:focused { -fx-text-fill: -bs-rd-error-red; } +.icon { + -fx-fill: -bs-text-color; +} + .opaque-icon { -fx-fill: -bs-color-gray-bbb; -fx-opacity: 1; @@ -1237,6 +1241,14 @@ textfield */ -jfx-rippler-fill: -fx-accent; } +.tab:disabled .jfx-rippler { + -jfx-rippler-fill: none !important; +} + +.tab:disabled .tab-label { + -fx-cursor: default !important; +} + .jfx-tab-pane .headers-region .tab .tab-container .tab-close-button > .jfx-svg-glyph { -fx-shape: "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z"; -jfx-size: 9; @@ -1267,6 +1279,7 @@ textfield */ -fx-padding: 14; -fx-font-size: 0.769em; -fx-font-weight: normal; + -fx-cursor: hand; } .jfx-tab-pane .depth-container { diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 28887daaab..aacd4c6b1b 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -39,10 +39,6 @@ -fx-image: url("../../images/remove.png"); } -#image-edit { - -fx-image: url("../../images/edit.png"); -} - #image-buy-white { -fx-image: url("../../images/buy_white.png"); } @@ -59,6 +55,10 @@ -fx-image: url("../../images/sell_red.png"); } +#image-lock2x { + -fx-image: url("../../images/lock@2x.png"); +} + #image-expand { -fx-image: url("../../images/expand.png"); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index fe18d711af..7856a2021f 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -20,6 +20,7 @@ package haveno.desktop.main; import com.google.inject.Inject; import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXComboBox; +import haveno.common.app.Version; import haveno.common.HavenoException; import haveno.common.Timer; import haveno.common.UserThread; @@ -165,14 +166,14 @@ public class MainView extends InitializableView { 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.buy").toUpperCase()); - ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sell").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 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 settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); @@ -198,10 +199,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(); } }); } @@ -304,8 +305,8 @@ public class MainView extends InitializableView { primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); - HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButtonWithBadge, - getNavigationSpacer(), accountButton, getNavigationSpacer()); + HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), accountButton, + getNavigationSpacer(), settingsButtonWithBadge, getNavigationSpacer()); secondaryNav.getStyleClass().add("nav-secondary"); HBox.setHgrow(secondaryNav, Priority.SOMETIMES); @@ -352,7 +353,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(); @@ -621,7 +622,9 @@ public class MainView extends InitializableView { splashP2PNetworkBox.setPrefHeight(40); splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton); - vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox); + Label versionLabel = new Label("v" + Version.VERSION); + + vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox, versionLabel); return vBox; } @@ -674,6 +677,7 @@ public class MainView extends InitializableView { } } else { xmrInfoLabel.setId("footer-pane"); + xmrInfoLabel.getStyleClass().remove("error-text"); if (xmrNetworkWarnMsgPopup != null) xmrNetworkWarnMsgPopup.hide(); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 4681431b70..03c38e99ab 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -53,6 +53,7 @@ import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; +import haveno.desktop.app.HavenoApp; import haveno.desktop.common.model.ViewModel; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.account.AccountView; @@ -140,6 +141,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @SuppressWarnings("FieldCanBeLocal") private MonadicBinding tradesAndUIReady; private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); + private Popup moneroConnectionErrorPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -226,6 +228,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" + trade.getInitError().getMessage()) .show(); + return; } // check trade period @@ -334,9 +337,93 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> { + if (connectionError == null) { + if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); + } else { + switch (connectionError) { + case LOCAL: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.localNode")) + .actionButtonText(Res.get("xmrConnectionError.localNode.start")) + .onAction(() -> { + log.warn("User has chosen to start local node."); + new Thread(() -> { + try { + HavenoUtils.xmrConnectionService.startLocalNode(); + } catch (Exception e) { + log.error("Error starting local node: {}", e.getMessage(), e); + new Popup() + .headLine(Res.get("xmrConnectionError.localNode.start.error")) + .warning(e.getMessage()) + .closeButtonText(Res.get("shared.close")) + .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."); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); + }) + .closeButtonText(Res.get("shared.shutDown")) + .onClose(HavenoApp.getShutDownHandler()); + break; + case CUSTOM: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.customNodes")) + .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; + 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; + } + moneroConnectionErrorPopup.show(); + } + }); + havenoSetup.setDisplayTorNetworkSettingsHandler(show -> { if (show) { torNetworkSettingsWindow.show(); + + // bring connection error popup to front if displayed + if (moneroConnectionErrorPopup != null && moneroConnectionErrorPopup.isDisplayed()) { + moneroConnectionErrorPopup.hide(); + moneroConnectionErrorPopup.show(); + } } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); } @@ -390,7 +477,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(this::showPopupIfInvalidXmrConfig); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { // We copy the array as we will mutate it later @@ -506,7 +593,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener }); } - private void showPopupIfInvalidBtcConfig() { + private void showPopupIfInvalidXmrConfig() { preferences.setMoneroNodesOptionOrdinal(0); new Popup().warning(Res.get("settings.net.warn.invalidXmrConfig")) .hideCloseButton() diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java b/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java index e59309c75f..7db3d7ddab 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/password/PasswordView.java @@ -39,7 +39,6 @@ import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel; import static haveno.desktop.util.FormBuilder.addMultilineLabel; import static haveno.desktop.util.FormBuilder.addPasswordTextField; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; -import org.apache.commons.lang3.exception.ExceptionUtils; import haveno.desktop.util.Layout; import haveno.desktop.util.validation.PasswordValidator; import javafx.beans.value.ChangeListener; @@ -160,7 +159,7 @@ public class PasswordView extends ActivatableView { } catch (Throwable t) { log.error("Error applying password: {}\n", t.getMessage(), t); new Popup() - .warning(Res.get("password.walletEncryptionFailed") + "\n\n" + ExceptionUtils.getStackTrace(t)) + .warning(Res.get("password.walletEncryptionFailed") + "\n\n" + t.getMessage()) .show(); } } 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 ae2b0a1bf9..a5f7332c81 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 @@ -38,6 +38,7 @@ import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccountFactory; +import haveno.core.payment.PaysafeAccount; import haveno.core.payment.RevolutAccount; import haveno.core.payment.USPostalMoneyOrderAccount; import haveno.core.payment.VenmoAccount; @@ -103,6 +104,7 @@ import haveno.desktop.components.paymentmethods.PaxumForm; import haveno.desktop.components.paymentmethods.PayByMailForm; import haveno.desktop.components.paymentmethods.PayPalForm; import haveno.desktop.components.paymentmethods.PaymentMethodForm; +import haveno.desktop.components.paymentmethods.PaysafeForm; import haveno.desktop.components.paymentmethods.PayseraForm; import haveno.desktop.components.paymentmethods.PaytmForm; import haveno.desktop.components.paymentmethods.PerfectMoneyForm; @@ -136,6 +138,7 @@ import haveno.desktop.util.FormBuilder; import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.add3ButtonsAfterGroup; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; +import static haveno.desktop.util.FormBuilder.addLabel; import static haveno.desktop.util.FormBuilder.addTopLabelListView; import haveno.desktop.util.GUIUtil; import haveno.desktop.util.Layout; @@ -274,7 +277,7 @@ public class TraditionalAccountsView extends PaymentAccountsView GUIUtil.openWebPage("https://haveno.exchange/wiki/Face-to-face_(payment_method)")) + .onClose(() -> GUIUtil.openWebPage("https://docs.haveno.exchange/the-project/payment_methods/F2F")) .actionButtonText(Res.get("shared.iUnderstand")) .onAction(() -> doSaveNewAccount(paymentAccount)) .show(); @@ -399,6 +402,13 @@ public class TraditionalAccountsView extends PaymentAccountsView doSaveNewAccount(paymentAccount)) .show(); + } else if (paymentAccount instanceof PaysafeAccount) { + new Popup().warning(Res.get("payment.paysafe.info")) + .width(700) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.iUnderstand")) + .onAction(() -> doSaveNewAccount(paymentAccount)) + .show(); } else { doSaveNewAccount(paymentAccount); } @@ -463,8 +473,9 @@ public class TraditionalAccountsView extends PaymentAccountsView { FXCollections.observableArrayList(Arrays.asList( ValidateOffer.class, MakerReserveOfferFunds.class, - AddToOfferBook.class) + MaybeAddToOfferBook.class) )); diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java index 180b883d61..9e21f19b5f 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java @@ -60,7 +60,7 @@ class DepositListItem { this.xmrWalletService = xmrWalletService; this.addressEntry = addressEntry; - balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); + balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex(), true); balance.set(HavenoUtils.formatXmr(balanceAsBI)); updateUsage(addressEntry.getSubaddressIndex()); 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 e7c4c89ab9..884df454e7 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 @@ -346,7 +346,7 @@ public class DepositView extends ActivatableView { List addressEntries = xmrWalletService.getAddressEntries(); List items = new ArrayList<>(); for (XmrAddressEntry addressEntry : addressEntries) { - if (addressEntry.isTrade()) continue; // skip reserved for trade + if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses items.add(new DepositListItem(addressEntry, xmrWalletService, formatter)); } 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 0a986b5505..8cf5700cc6 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 @@ -225,8 +225,8 @@ public class LockedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } 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 bfa8bd26b0..bcef7e6488 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 @@ -224,8 +224,8 @@ public class ReservedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } 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 7c5da97808..24d8f121d7 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 @@ -38,8 +38,8 @@ + - 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 284d336904..b882782212 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 @@ -70,7 +70,7 @@ public class TransactionsView extends ActivatableView { @FXML TableView tableView; @FXML - TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, memoColumn, confidenceColumn, revertTxColumn; + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, confidenceColumn, memoColumn, revertTxColumn; @FXML Label numItems; @FXML @@ -133,8 +133,8 @@ public class TransactionsView extends ActivatableView { transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode()))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); - memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); 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); @@ -146,8 +146,8 @@ public class TransactionsView extends ActivatableView { setTransactionColumnCellFactory(); setAmountColumnCellFactory(); setTxFeeColumnCellFactory(); - setMemoColumnCellFactory(); setConfidenceColumnCellFactory(); + setMemoColumnCellFactory(); setRevertTxColumnCellFactory(); dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate)); @@ -162,8 +162,7 @@ public class TransactionsView extends ActivatableView { transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId)); amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmount)); confidenceColumn.setComparator(Comparator.comparingLong(TransactionsListItem::getNumConfirmations)); - memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo)); - + memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo, Comparator.nullsLast(Comparator.naturalOrder()))); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); @@ -222,8 +221,8 @@ public class TransactionsView extends ActivatableView { columns[3] = item.getTxId(); columns[4] = item.getAmountStr(); columns[5] = item.getTxFeeStr(); - columns[6] = item.getMemo() == null ? "" : item.getMemo(); - columns[7] = String.valueOf(item.getNumConfirmations()); + columns[6] = String.valueOf(item.getNumConfirmations()); + columns[7] = item.getMemo() == null ? "" : item.getMemo(); return columns; }; diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java index ed9a0f0a7d..add84853c3 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalListItem.java @@ -77,7 +77,7 @@ class WithdrawalListItem { public final String getLabel() { if (addressEntry.isOpenOffer()) return Res.getWithCol("shared.offerId") + " " + addressEntry.getShortOfferId(); - else if (addressEntry.isTrade()) + else if (addressEntry.isTradePayout()) return Res.getWithCol("shared.tradeId") + " " + addressEntry.getShortOfferId(); else if (addressEntry.getContext() == XmrAddressEntry.Context.ARBITRATOR) return Res.get("funds.withdrawal.arbitrationFee"); 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 53b3d70bbf..730b47adf6 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 @@ -302,10 +302,9 @@ public class WithdrawalView extends ActivatableView { BigInteger receiverAmount = tx.getOutgoingTransfer().getDestinations().get(0).getAmount(); BigInteger fee = tx.getFee(); String messageText = Res.get("shared.sendFundsDetailsWithFee", - HavenoUtils.formatXmr(receiverAmount.add(fee), true), + HavenoUtils.formatXmr(receiverAmount, true), withdrawToAddress, - HavenoUtils.formatXmr(fee, true), - HavenoUtils.formatXmr(receiverAmount, true)); + HavenoUtils.formatXmr(fee, true)); // popup confirmation message Popup popup = new Popup(); 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 ac57c22bce..3d2ec6d884 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 @@ -207,16 +207,21 @@ public class OfferBookChartView extends ActivatableViewAndModel { + timeUnitChangeListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { if (newValue != null) { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } - }; - priceAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + priceAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { priceAxisYWidth = (double) newValue; layoutChart(); - }; - volumeAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + volumeAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { volumeAxisYWidth = (double) newValue; layoutChart(); - }; - tradeStatisticsByCurrencyListener = c -> { + }); + tradeStatisticsByCurrencyListener = c -> UserThread.execute(() -> { nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); fillList(); - }; - parentHeightListener = (observable, oldValue, newValue) -> layout(); + }); + parentHeightListener = (observable, oldValue, newValue) -> UserThread.execute(this::layout); - priceColumnLabelListener = (o, oldVal, newVal) -> priceColumn.setGraphic(new AutoTooltipLabel(newVal)); + priceColumnLabelListener = (o, oldVal, newVal) -> UserThread.execute(() -> priceColumn.setGraphic(new AutoTooltipLabel(newVal))); // Need to render on next frame as otherwise there are issues in the chart rendering itemsChangeListener = c -> UserThread.execute(this::updateChartData); @@ -238,31 +238,34 @@ public class TradesChartsView extends ActivatableViewAndModel { - priceChart.setVisible(!showAll); - priceChart.setManaged(!showAll); - priceColumn.setSortable(!showAll); + UserThread.execute(() -> { + priceChart.setVisible(!showAll); + priceChart.setManaged(!showAll); + priceColumn.setSortable(!showAll); - if (showAll) { - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); - priceColumnLabel.set(Res.get("shared.price")); - if (!tableView.getColumns().contains(marketColumn)) - tableView.getColumns().add(1, marketColumn); + if (showAll) { + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); + priceColumnLabel.set(Res.get("shared.price")); + if (!tableView.getColumns().contains(marketColumn)) + tableView.getColumns().add(1, marketColumn); + + volumeChart.setPrefHeight(volumeChart.getMaxHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); + } else { + volumeChart.setPrefHeight(volumeChart.getMinHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); + priceSeries.setName(selectedTradeCurrency.getName()); + String code = selectedTradeCurrency.getCode(); + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); + + priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); + + tableView.getColumns().remove(marketColumn); + } + + layout(); + }); - volumeChart.setPrefHeight(volumeChart.getMaxHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); - } else { - volumeChart.setPrefHeight(volumeChart.getMinHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); - priceSeries.setName(selectedTradeCurrency.getName()); - String code = selectedTradeCurrency.getCode(); - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); - - priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); - - tableView.getColumns().remove(marketColumn); - } - - layout(); return null; }); } @@ -286,14 +289,14 @@ public class TradesChartsView extends ActivatableViewAndModel { + currencyComboBox.setOnChangeConfirmed(e -> UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) currencyComboBox.getSelectionModel().select(SHOW_ALL); CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); } - }); + })); toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.java index c5def765e9..67591cbce4 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/BuyOfferView.java @@ -18,6 +18,8 @@ package haveno.desktop.main.offer; import com.google.inject.Inject; + +import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.user.Preferences; import haveno.core.user.User; @@ -42,4 +44,9 @@ public class BuyOfferView extends OfferView { p2PService, OfferDirection.BUY); } + + @Override + protected String getOfferLabel() { + return Res.get("offerbook.buyXmrWith"); + } } 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 d48a1d2e2d..47bfee0006 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -20,6 +20,8 @@ package haveno.desktop.main.offer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.util.MathUtils; import haveno.common.util.Utilities; @@ -57,7 +59,6 @@ import java.util.Comparator; import static java.util.Comparator.comparing; import java.util.Date; import java.util.HashSet; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -81,7 +82,7 @@ import lombok.Getter; import org.jetbrains.annotations.NotNull; public abstract class MutableOfferDataModel extends OfferDataModel { - private final CreateOfferService createOfferService; + protected final CreateOfferService createOfferService; protected final OpenOfferManager openOfferManager; private final XmrWalletService xmrWalletService; private final Preferences preferences; @@ -104,15 +105,17 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected final ObjectProperty price = new SimpleObjectProperty<>(); protected final ObjectProperty volume = new SimpleObjectProperty<>(); protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); + protected final ObjectProperty extraInfo = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount - protected final DoubleProperty buyerSecurityDepositPct = new SimpleDoubleProperty(); + protected final DoubleProperty securityDepositPct = new SimpleDoubleProperty(); + protected final BooleanProperty buyerAsTakerWithoutDeposit = new SimpleBooleanProperty(); protected final ObservableList paymentAccounts = FXCollections.observableArrayList(); protected PaymentAccount paymentAccount; boolean isTabSelected; - protected double marketPriceMargin = 0; + protected double marketPriceMarginPct = 0; @Getter private boolean marketPriceAvailable; protected boolean allowAmountUpdate = true; @@ -165,7 +168,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { reserveExactAmount = preferences.getSplitOfferOutput(); useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice()); - buyerSecurityDepositPct.set(Restrictions.getMinBuyerSecurityDepositAsPercent()); + securityDepositPct.set(Restrictions.getMinSecurityDepositAsPercent()); paymentAccountsChangeListener = change -> fillPaymentAccounts(); } @@ -177,7 +180,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { if (isTabSelected) priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); - updateBalance(); + updateBalances(); } @Override @@ -186,12 +189,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } private void addListeners() { - xmrWalletService.addBalanceListener(xmrBalanceListener); + if (xmrBalanceListener != null) xmrWalletService.addBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener); } private void removeListeners() { - xmrWalletService.removeBalanceListener(xmrBalanceListener); + if (xmrBalanceListener != null) xmrWalletService.removeBalanceListener(xmrBalanceListener); user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener); } @@ -201,14 +204,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel { /////////////////////////////////////////////////////////////////////////////////////////// // called before activate() - public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { - addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); - xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { - @Override - public void onBalanceChanged(BigInteger balance) { - updateBalance(); - } - }; + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { + if (initAddressEntry) { + addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); + xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) { + @Override + public void onBalanceChanged(BigInteger balance) { + updateBalances(); + } + }; + } this.direction = direction; this.tradeCurrency = tradeCurrency; @@ -247,7 +252,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { calculateVolume(); calculateTotalToPay(); - updateBalance(); + updateBalances(); setSuggestedSecurityDeposit(getPaymentAccount()); return true; @@ -257,10 +262,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { private Optional getAnyPaymentAccount() { if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) { return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isFiat()).findAny(); + } else if (CurrencyUtil.isCryptoCurrency(tradeCurrency.getCode())) { + return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isCryptoCurrency()).findAny(); } else { - return paymentAccounts.stream().filter(paymentAccount1 -> !paymentAccount1.isFiat() && - paymentAccount1.getTradeCurrency().isPresent() && - !Objects.equals(paymentAccount1.getTradeCurrency().get().getCode(), GUIUtil.TOP_CRYPTO.getCode())).findAny(); + return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.getTradeCurrency().isPresent()).findAny(); } } @@ -274,6 +279,20 @@ public abstract class MutableOfferDataModel extends OfferDataModel { priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } + protected void updateBalances() { + if (addressEntry == null) return; + super.updateBalances(); + + // update remaining balance + UserThread.await(() -> { + missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); + isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), balance.get())); + if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { + showWalletFundedNotification.set(true); + } + }); + } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// @@ -286,9 +305,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { minAmount.get(), useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), - useMarketBasedPrice.get() ? marketPriceMargin : 0, - buyerSecurityDepositPct.get(), - paymentAccount); + useMarketBasedPrice.get() ? marketPriceMarginPct : 0, + securityDepositPct.get(), + paymentAccount, + buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit + buyerAsTakerWithoutDeposit.get(), + extraInfo.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -297,6 +319,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { triggerPrice, reserveExactAmount, false, // desktop ui resets address entries on cancel + null, resultHandler, errorMessageHandler); } @@ -315,10 +338,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { - var minSecurityDeposit = Restrictions.getMinBuyerSecurityDepositAsPercent(); + var minSecurityDeposit = Restrictions.getMinSecurityDepositAsPercent(); try { if (getTradeCurrency() == null) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // Get average historic prices over for the prior trade period equaling the lock time @@ -341,16 +364,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel { var min = extremes[0]; var max = extremes[1]; if (min == 0d || max == 0d) { - setBuyerSecurityDeposit(minSecurityDeposit); + setSecurityDepositPct(minSecurityDeposit); return; } // 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.getMaxBuyerSecurityDepositAsPercent()); - buyerSecurityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); + Math.min(2 * (max - min) / max, Restrictions.getMaxSecurityDepositAsPercent()); + securityDepositPct.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit)); } catch (Throwable t) { log.error(t.toString()); - buyerSecurityDepositPct.set(minSecurityDeposit); + securityDepositPct.set(minSecurityDeposit); } } @@ -368,7 +391,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { volume.set(null); minVolume.set(null); price.set(null); - marketPriceMargin = 0; + marketPriceMarginPct = 0; } this.tradeCurrency = tradeCurrency; @@ -394,15 +417,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { void fundFromSavingsWallet() { this.useSavingsWallet = true; - updateBalance(); - if (!isXmrWalletFunded.get()) { - this.useSavingsWallet = false; - updateBalance(); - } - } - - protected void setMarketPriceMarginPct(double marketPriceMargin) { - this.marketPriceMargin = marketPriceMargin; + updateBalances(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -445,23 +460,27 @@ public abstract class MutableOfferDataModel extends OfferDataModel { preferences.setUsePercentageBasedPrice(useMarketBasedPrice); } + protected void setBuyerAsTakerWithoutDeposit(boolean buyerAsTakerWithoutDeposit) { + this.buyerAsTakerWithoutDeposit.set(buyerAsTakerWithoutDeposit); + } + public ObservableList getPaymentAccounts() { return paymentAccounts; } public double getMarketPriceMarginPct() { - return marketPriceMargin; + return marketPriceMarginPct; } long getMaxTradeLimit() { // 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); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY, buyerAsTakerWithoutDeposit.get()); } if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction); + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction, buyerAsTakerWithoutDeposit.get()); } else { return 0; } @@ -493,7 +512,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - updateBalance(); + updateBalances(); } void calculateMinVolume() { @@ -546,14 +565,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { BigInteger feeAndSecDeposit = getSecurityDeposit().add(makerFee); BigInteger total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get()); totalToPay.set(total); - updateBalance(); + updateBalances(); } } - BigInteger getSecurityDeposit() { - return isBuyOffer() ? getBuyerSecurityDeposit() : getSellerSecurityDeposit(); - } - void swapTradeToSavings() { xmrWalletService.resetAddressEntriesForOpenOffer(offerId); } @@ -570,6 +585,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.amount.set(amount); } + protected void setMinAmount(BigInteger minAmount) { + this.minAmount.set(minAmount); + } + protected void setPrice(Price price) { this.price.set(price); } @@ -578,8 +597,28 @@ public abstract class MutableOfferDataModel extends OfferDataModel { this.volume.set(volume); } - protected void setBuyerSecurityDeposit(double value) { - this.buyerSecurityDepositPct.set(value); + protected void setSecurityDepositPct(double value) { + this.securityDepositPct.set(value); + } + + public void setMarketPriceAvailable(boolean marketPriceAvailable) { + this.marketPriceAvailable = marketPriceAvailable; + } + + public void setTriggerPrice(long triggerPrice) { + this.triggerPrice = triggerPrice; + } + + public void setMarketPriceMarginPct(double marketPriceMarginPct) { + this.marketPriceMarginPct = marketPriceMarginPct; + } + + public void setReserveExactAmount(boolean reserveExactAmount) { + this.reserveExactAmount = reserveExactAmount; + } + + protected void setExtraInfo(String extraInfo) { + this.extraInfo.set(extraInfo); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -610,8 +649,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return minVolume; } - protected void setMinAmount(BigInteger minAmount) { - this.minAmount.set(minAmount); + public ReadOnlyBooleanProperty getBuyerAsTakerWithoutDeposit() { + return buyerAsTakerWithoutDeposit; } public ReadOnlyStringProperty getTradeCurrencyCode() { @@ -634,47 +673,35 @@ public abstract class MutableOfferDataModel extends OfferDataModel { return useMarketBasedPrice; } - ReadOnlyDoubleProperty getBuyerSecurityDepositPct() { - return buyerSecurityDepositPct; + ReadOnlyDoubleProperty getSecurityDepositPct() { + return securityDepositPct; } - protected BigInteger getBuyerSecurityDeposit() { - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDepositPct.get(), amount.get()); - return getBoundedBuyerSecurityDeposit(percentOfAmount); - } - - private BigInteger getSellerSecurityDeposit() { + protected BigInteger getSecurityDeposit() { BigInteger amount = this.amount.get(); - if (amount == null) - amount = BigInteger.ZERO; - - BigInteger percentOfAmount = CoinUtil.getPercentOfAmount( - createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDepositPct.get()), amount); - return getBoundedSellerSecurityDeposit(percentOfAmount); + if (amount == null) amount = BigInteger.ZERO; + BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(securityDepositPct.get(), amount); + return getBoundedSecurityDeposit(percentOfAmount); } - protected BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinBuyerSecurityDeposit from Restrictions. - return Restrictions.getMinBuyerSecurityDeposit().max(value); + protected BigInteger getBoundedSecurityDeposit(BigInteger value) { + return Restrictions.getMinSecurityDeposit().max(value); } - private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) { - // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the - // MinSellerSecurityDeposit from Restrictions. - return Restrictions.getMinSellerSecurityDeposit().max(value); + protected double getSecurityAsPercent(Offer offer) { + BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit()); + double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit, + offer.getAmount()); + return Math.min(offerSellerSecurityDepositAsPercent, + Restrictions.getMaxSecurityDepositAsPercent()); } ReadOnlyObjectProperty totalToPayAsProperty() { return totalToPay; } - public void setMarketPriceAvailable(boolean marketPriceAvailable) { - this.marketPriceAvailable = marketPriceAvailable; - } - public BigInteger getMaxMakerFee() { - return HavenoUtils.multiply(amount.get(), HavenoUtils.MAKER_FEE_PCT); + return HavenoUtils.multiply(amount.get(), buyerAsTakerWithoutDeposit.get() ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT); } boolean canPlaceOffer() { @@ -682,15 +709,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel { GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); } - public boolean isMinBuyerSecurityDeposit() { - return getBuyerSecurityDeposit().compareTo(Restrictions.getMinBuyerSecurityDeposit()) <= 0; + public boolean isMinSecurityDeposit() { + return getSecurityDeposit().compareTo(Restrictions.getMinSecurityDeposit()) <= 0; } - public void setTriggerPrice(long triggerPrice) { - this.triggerPrice = triggerPrice; - } - - public void setReserveExactAmount(boolean reserveExactAmount) { - this.reserveExactAmount = reserveExactAmount; + public ReadOnlyObjectProperty getExtraInfo() { + return extraInfo; } } 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 db008e93ff..e28ecff72b 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -45,6 +45,7 @@ import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; import haveno.desktop.components.InfoInputTextField; +import haveno.desktop.components.InputTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; @@ -71,12 +72,12 @@ import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -126,22 +127,24 @@ public abstract class MutableOfferView> exten private ScrollPane scrollPane; protected GridPane gridPane; - private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, paymentTitledGroupBg; + private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, extraInfoTitledGroupBg, paymentTitledGroupBg; protected TitledGroupBg amountTitledGroupBg; private BusyAnimation waitingForFundsSpinner; private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton, fundFromSavingsWalletButton; private Button priceTypeToggleButton; private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; - protected InputTextField amountTextField, minAmountTextField, volumeTextField, buyerSecurityDepositInputTextField; + protected InputTextField amountTextField, minAmountTextField, volumeTextField, securityDepositInputTextField; private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; - private CheckBox reserveExactAmountCheckbox; + private ToggleButton reserveExactAmountSlider; + private ToggleButton buyerAsTakerWithoutDepositSlider; + protected InputTextArea extraInfoTextArea; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, - resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, buyerSecurityDepositLabel, - buyerSecurityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; + resultLabel, tradeFeeInXmrLabel, xLabel, fakeXLabel, securityDepositLabel, + securityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; @@ -149,16 +152,16 @@ public abstract class MutableOfferView> exten private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, - minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox; + minAmountValueCurrencyBox, securityDepositAndFeeBox, triggerPriceHBox; private Subscription isWaitingForFundsSubscription, balanceSubscription; private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, - buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, + securityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, - isMinBuyerSecurityDepositListener, triggerPriceFocusedListener; + isMinSecurityDepositListener, buyerAsTakerWithoutDepositListener, triggerPriceFocusedListener, extraInfoFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, - marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener; + marketPriceMarginListener, volumeListener, securityDepositInXMRListener, extraInfoListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; @@ -168,12 +171,14 @@ public abstract class MutableOfferView> exten private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); private boolean zelleWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, - buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField; + securityDepositInfoInputTextField, triggerPriceInfoInputTextField; private Text xIcon, fakeXIcon; @Setter private OfferView.OfferActionHandler offerActionHandler; + private int heightAdjustment = -5; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -199,11 +204,13 @@ public abstract class MutableOfferView> exten addPaymentGroup(); addAmountPriceGroup(); addOptionsGroup(); + addExtraInfoGroup(); + addNextButtons(); addFundingGroup(); createListeners(); - balanceTextField.setFormatter(model.getBtcFormatter()); + balanceTextField.setFormatter(model.getXmrFormatter()); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"), @@ -252,6 +259,11 @@ public abstract class MutableOfferView> exten Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip")); triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); + + buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); + + triggerPriceInputTextField.setText(model.triggerPrice.get()); + extraInfoTextArea.setText(model.dataModel.extraInfo.get()); } } @@ -285,11 +297,13 @@ public abstract class MutableOfferView> exten model.getDataModel().onTabSelected(isSelected); } - public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, + public void initWithData(OfferDirection direction, + TradeCurrency tradeCurrency, + boolean initAddressEntry, OfferView.OfferActionHandler offerActionHandler) { this.offerActionHandler = offerActionHandler; - boolean result = model.initWithData(direction, tradeCurrency); + boolean result = model.initWithData(direction, tradeCurrency, initAddressEntry); if (!result) { new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) @@ -308,7 +322,7 @@ public abstract class MutableOfferView> exten if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.buy")); } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), tradeCurrency.getCode()); + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), tradeCurrency.getCode()); } nextButton.setId("buy-button"); fundFromSavingsWalletButton.setId("buy-button"); @@ -317,12 +331,15 @@ public abstract class MutableOfferView> exten if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { placeOfferButtonLabel = Res.get("createOffer.placeOfferButton", Res.get("shared.sell")); } else { - placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), tradeCurrency.getCode()); + placeOfferButtonLabel = Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), tradeCurrency.getCode()); } nextButton.setId("sell-button"); fundFromSavingsWalletButton.setId("sell-button"); } + buyerAsTakerWithoutDepositSlider.setVisible(model.isSellOffer()); + buyerAsTakerWithoutDepositSlider.setManaged(model.isSellOffer()); + placeOfferButton.updateText(placeOfferButtonLabel); updatePriceToggle(); } @@ -363,8 +380,6 @@ public abstract class MutableOfferView> exten } private void onShowPayFundsScreen() { - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - nextButton.setVisible(false); nextButton.setManaged(false); nextButton.setOnAction(null); @@ -375,8 +390,16 @@ public abstract class MutableOfferView> exten setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); + + extraInfoTitledGroupBg.setVisible(false); + extraInfoTitledGroupBg.setManaged(false); + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); updateQrCode(); @@ -420,13 +443,7 @@ 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); - reserveExactAmountCheckbox.setVisible(true); + showFundingGroup(); } private void updateOfferElementsStyle() { @@ -556,10 +573,11 @@ public abstract class MutableOfferView> exten volumeTextField.promptTextProperty().bind(model.volumePromptLabel); totalToPayTextField.textProperty().bind(model.totalToPay); addressTextField.amountAsProperty().bind(model.getDataModel().getMissingCoin()); - buyerSecurityDepositInputTextField.textProperty().bindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().bind(model.buyerSecurityDepositLabel); + securityDepositInputTextField.textProperty().bindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().bind(model.securityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); + extraInfoTextArea.textProperty().bindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().bind(model.amountValidationResult); @@ -567,7 +585,8 @@ public abstract class MutableOfferView> exten fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult); triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); - buyerSecurityDepositInputTextField.validationResultProperty().bind(model.buyerSecurityDepositValidationResult); + securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); + extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); @@ -604,12 +623,13 @@ public abstract class MutableOfferView> exten volumeTextField.promptTextProperty().unbindBidirectional(model.volume); totalToPayTextField.textProperty().unbind(); addressTextField.amountAsProperty().unbind(); - buyerSecurityDepositInputTextField.textProperty().unbindBidirectional(model.buyerSecurityDeposit); - buyerSecurityDepositLabel.textProperty().unbind(); + securityDepositInputTextField.textProperty().unbindBidirectional(model.securityDeposit); + securityDepositLabel.textProperty().unbind(); tradeFeeInXmrLabel.textProperty().unbind(); tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.visibleProperty().unbind(); + extraInfoTextArea.textProperty().unbindBidirectional(model.extraInfo); // Validation amountTextField.validationResultProperty().unbind(); @@ -617,7 +637,7 @@ public abstract class MutableOfferView> exten fixedPriceTextField.validationResultProperty().unbind(); triggerPriceInputTextField.validationResultProperty().unbind(); volumeTextField.validationResultProperty().unbind(); - buyerSecurityDepositInputTextField.validationResultProperty().unbind(); + securityDepositInputTextField.validationResultProperty().unbind(); // funding fundingHBox.visibleProperty().unbind(); @@ -679,15 +699,18 @@ public abstract class MutableOfferView> exten model.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(model.volume.get()); }; - buyerSecurityDepositFocusedListener = (o, oldValue, newValue) -> { - model.onFocusOutBuyerSecurityDepositTextField(oldValue, newValue); - buyerSecurityDepositInputTextField.setText(model.buyerSecurityDeposit.get()); + securityDepositFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutSecurityDepositTextField(oldValue, newValue); + securityDepositInputTextField.setText(model.securityDeposit.get()); }; - triggerPriceFocusedListener = (o, oldValue, newValue) -> { model.onFocusOutTriggerPriceTextField(oldValue, newValue); triggerPriceInputTextField.setText(model.triggerPrice.get()); }; + extraInfoFocusedListener = (observable, oldValue, newValue) -> { + model.onFocusOutExtraInfoTextArea(oldValue, newValue); + extraInfoTextArea.setText(model.extraInfo.get()); + }; errorMessageListener = (o, oldValue, newValue) -> { if (model.createOfferCanceled) return; @@ -707,10 +730,10 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.clear(); if (!CurrencyUtil.isTraditionalCurrency(newValue)) { if (model.isShownAsBuyOffer()) { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), model.getTradeCurrency().getCode())); } else { - placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.sell"), + placeOfferButton.updateText(Res.get("createOffer.placeOfferButtonCrypto", Res.get("shared.buy"), model.getTradeCurrency().getCode())); } } @@ -750,12 +773,11 @@ public abstract class MutableOfferView> exten } }; - buyerSecurityDepositInBTCListener = (observable, oldValue, newValue) -> { + securityDepositInXMRListener = (observable, oldValue, newValue) -> { if (!newValue.equals("")) { - Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(newValue)); - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + updateSecurityDepositLabels(); } else { - buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(null); + securityDepositInfoInputTextField.setContentForInfoPopOver(null); } }; @@ -805,17 +827,35 @@ public abstract class MutableOfferView> exten } }; - isMinBuyerSecurityDepositListener = ((observable, oldValue, newValue) -> { - if (newValue) { - // show BTC - buyerSecurityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); - buyerSecurityDepositInputTextField.setDisable(true); - } else { - // show % - buyerSecurityDepositPercentageLabel.setText("%"); - buyerSecurityDepositInputTextField.setDisable(false); - } + isMinSecurityDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); }); + + buyerAsTakerWithoutDepositListener = ((observable, oldValue, newValue) -> { + updateSecurityDepositLabels(); + }); + + extraInfoListener = (observable, oldValue, newValue) -> { + if (newValue != null && !newValue.equals("")) { + // no action + } + }; + } + + private void updateSecurityDepositLabels() { + if (model.isMinSecurityDeposit.get()) { + // show XMR + securityDepositPercentageLabel.setText(Res.getBaseCurrencyCode()); + securityDepositInputTextField.setDisable(true); + } else { + // show % + securityDepositPercentageLabel.setText("%"); + securityDepositInputTextField.setDisable(model.getDataModel().buyerAsTakerWithoutDeposit.get()); + } + if (model.securityDepositInXMR.get() != null && !model.securityDepositInXMR.get().equals("")) { + Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(model.securityDepositInXMR.get())); + securityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); + } } private void updateQrCode() { @@ -856,8 +896,10 @@ public abstract class MutableOfferView> exten model.marketPriceMargin.addListener(marketPriceMarginListener); model.volume.addListener(volumeListener); model.getDataModel().missingCoin.addListener(missingCoinListener); - model.buyerSecurityDepositInBTC.addListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.addListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.addListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.addListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.addListener(buyerAsTakerWithoutDepositListener); + model.getDataModel().extraInfo.addListener(extraInfoListener); // focus out amountTextField.focusedProperty().addListener(amountFocusedListener); @@ -866,7 +908,8 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.focusedProperty().addListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().addListener(securityDepositFocusedListener); + extraInfoTextArea.focusedProperty().addListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); @@ -888,8 +931,10 @@ public abstract class MutableOfferView> exten model.marketPriceMargin.removeListener(marketPriceMarginListener); model.volume.removeListener(volumeListener); model.getDataModel().missingCoin.removeListener(missingCoinListener); - model.buyerSecurityDepositInBTC.removeListener(buyerSecurityDepositInBTCListener); - model.isMinBuyerSecurityDeposit.removeListener(isMinBuyerSecurityDepositListener); + model.securityDepositInXMR.removeListener(securityDepositInXMRListener); + model.isMinSecurityDeposit.removeListener(isMinSecurityDepositListener); + model.getDataModel().buyerAsTakerWithoutDeposit.removeListener(buyerAsTakerWithoutDepositListener); + model.getDataModel().extraInfo.removeListener(extraInfoListener); // focus out amountTextField.focusedProperty().removeListener(amountFocusedListener); @@ -898,7 +943,8 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.focusedProperty().removeListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); - buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener); + securityDepositInputTextField.focusedProperty().removeListener(securityDepositFocusedListener); + extraInfoTextArea.focusedProperty().removeListener(extraInfoFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); @@ -932,10 +978,11 @@ public abstract class MutableOfferView> exten gridPane.setVgap(5); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); scrollPane.setContent(gridPane); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); } private void addPaymentGroup() { - paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer")); + paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 1, Res.get("offerbook.createOffer"), heightAdjustment); GridPane.setColumnSpan(paymentTitledGroupBg, 2); HBox paymentGroupBox = new HBox(); @@ -953,7 +1000,7 @@ public abstract class MutableOfferView> exten GridPane.setRowIndex(paymentGroupBox, gridRow); GridPane.setColumnSpan(paymentGroupBox, 2); - GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); + GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE + heightAdjustment, 0, 0, 0)); gridPane.getChildren().add(paymentGroupBox); tradingAccountBoxTuple.first.setMinWidth(800); @@ -989,7 +1036,7 @@ public abstract class MutableOfferView> exten private void addAmountPriceGroup() { amountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, - Res.get("createOffer.setAmountPrice"), 25); + Res.get("createOffer.setAmountPrice"), 25 + heightAdjustment); GridPane.setColumnSpan(amountTitledGroupBg, 2); addAmountPriceFields(); @@ -997,23 +1044,72 @@ public abstract class MutableOfferView> exten } private void addOptionsGroup() { - setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, - Res.get("shared.advancedOptions"), Layout.COMPACT_GROUP_DISTANCE); + setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, + Res.get("shared.advancedOptions"), 25 + heightAdjustment); - advancedOptionsBox = new HBox(); - advancedOptionsBox.setSpacing(40); + securityDepositAndFeeBox = new HBox(); + securityDepositAndFeeBox.setSpacing(40); - GridPane.setRowIndex(advancedOptionsBox, gridRow); - GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING); - GridPane.setColumnIndex(advancedOptionsBox, 0); - GridPane.setHalignment(advancedOptionsBox, HPos.LEFT); - GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); - gridPane.getChildren().add(advancedOptionsBox); + GridPane.setRowIndex(securityDepositAndFeeBox, gridRow); + GridPane.setColumnSpan(securityDepositAndFeeBox, GridPane.REMAINING); + GridPane.setColumnIndex(securityDepositAndFeeBox, 0); + GridPane.setHalignment(securityDepositAndFeeBox, HPos.LEFT); + GridPane.setMargin(securityDepositAndFeeBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(securityDepositAndFeeBox); VBox tradeFeeFieldsBox = getTradeFeeFieldsBox(); tradeFeeFieldsBox.setMinWidth(240); - advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), tradeFeeFieldsBox); + securityDepositAndFeeBox.getChildren().addAll(getSecurityDepositBox(), tradeFeeFieldsBox); + buyerAsTakerWithoutDepositSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("createOffer.buyerAsTakerWithoutDeposit")); + buyerAsTakerWithoutDepositSlider.setPadding(new Insets(0, 0, 0, 0)); + buyerAsTakerWithoutDepositSlider.setOnAction(event -> { + + // popup info box + String key = "popup.info.buyerAsTakerWithoutDeposit"; + if (buyerAsTakerWithoutDepositSlider.isSelected() && DontShowAgainLookup.showAgain(key)) { + new Popup().headLine(Res.get(key + ".headline")) + .information(Res.get(key)) + .closeButtonText(Res.get("shared.cancel")) + .actionButtonText(Res.get("shared.ok")) + .onAction(() -> model.dataModel.setBuyerAsTakerWithoutDeposit(true)) + .onClose(() -> { + buyerAsTakerWithoutDepositSlider.setSelected(false); + model.dataModel.setBuyerAsTakerWithoutDeposit(false); + }) + .dontShowAgainId(key) + .show(); + } else { + model.dataModel.setBuyerAsTakerWithoutDeposit(buyerAsTakerWithoutDepositSlider.isSelected()); + } + }); + GridPane.setHalignment(buyerAsTakerWithoutDepositSlider, HPos.LEFT); + GridPane.setMargin(buyerAsTakerWithoutDepositSlider, new Insets(0, 0, 0, 0)); + } + + private void addExtraInfoGroup() { + + extraInfoTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, + Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment); + GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); + + extraInfoTextArea = new InputTextArea(); + extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); + extraInfoTextArea.getStyleClass().add("text-area"); + extraInfoTextArea.setWrapText(true); + extraInfoTextArea.setPrefHeight(75); + extraInfoTextArea.setMinHeight(75); + extraInfoTextArea.setMaxHeight(75); + extraInfoTextArea.setFocusTraversable(false); + GridPane.setRowIndex(extraInfoTextArea, gridRow); + GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); + GridPane.setColumnIndex(extraInfoTextArea, 0); + GridPane.setHalignment(extraInfoTextArea, HPos.LEFT); + GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0)); + gridPane.getChildren().add(extraInfoTextArea); + } + + private void addNextButtons() { Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("shared.nextStep"), Res.get("shared.cancel")); nextButton = (AutoTooltipButton) tuple.first; @@ -1056,30 +1152,76 @@ public abstract class MutableOfferView> exten protected void hideOptionsGroup() { setDepositTitledGroupBg.setVisible(false); setDepositTitledGroupBg.setManaged(false); + securityDepositAndFeeBox.setVisible(false); + securityDepositAndFeeBox.setManaged(false); + buyerAsTakerWithoutDepositSlider.setVisible(false); + buyerAsTakerWithoutDepositSlider.setManaged(false); + } + + protected void hideExtraInfoGroup() { + extraInfoTitledGroupBg.setVisible(false); + extraInfoTitledGroupBg.setManaged(false); + extraInfoTextArea.setVisible(false); + extraInfoTextArea.setManaged(false); + } + + protected void hideNextButtons() { nextButton.setVisible(false); nextButton.setManaged(false); cancelButton1.setVisible(false); cancelButton1.setManaged(false); - advancedOptionsBox.setVisible(false); - advancedOptionsBox.setManaged(false); } - private VBox getBuyerSecurityDepositBox() { + protected void hideFundingGroup() { + payFundsTitledGroupBg.setVisible(false); + payFundsTitledGroupBg.setManaged(false); + totalToPayTextField.setVisible(false); + totalToPayTextField.setManaged(false); + addressTextField.setVisible(false); + addressTextField.setManaged(false); + qrCodeImageView.setVisible(false); + qrCodeImageView.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); + qrCodeImageView.setVisible(true); + qrCodeImageView.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")); - buyerSecurityDepositInfoInputTextField = tuple.second; - buyerSecurityDepositInputTextField = buyerSecurityDepositInfoInputTextField.getInputTextField(); - buyerSecurityDepositPercentageLabel = tuple.third; + securityDepositInfoInputTextField = tuple.second; + securityDepositInputTextField = securityDepositInfoInputTextField.getInputTextField(); + securityDepositPercentageLabel = tuple.third; // getEditableValueBox delivers BTC, so we overwrite it with % - buyerSecurityDepositPercentageLabel.setText("%"); + securityDepositPercentageLabel.setText("%"); Tuple2 tradeInputBoxTuple = getTradeInputBox(tuple.first, model.getSecurityDepositLabel()); VBox depositBox = tradeInputBoxTuple.second; - buyerSecurityDepositLabel = tradeInputBoxTuple.first; + securityDepositLabel = tradeInputBoxTuple.first; depositBox.setMaxWidth(310); - editOfferElements.add(buyerSecurityDepositInputTextField); - editOfferElements.add(buyerSecurityDepositPercentageLabel); + editOfferElements.add(securityDepositInputTextField); + editOfferElements.add(securityDepositPercentageLabel); return depositBox; } @@ -1087,7 +1229,7 @@ public abstract class MutableOfferView> exten private void addFundingGroup() { // don't increase gridRow as we removed button when this gets visible payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, - Res.get("createOffer.fundsBox.title"), 25); + Res.get("createOffer.fundsBox.title"), 20 + heightAdjustment); payFundsTitledGroupBg.getStyleClass().add("last"); GridPane.setColumnSpan(payFundsTitledGroupBg, 2); payFundsTitledGroupBg.setVisible(false); @@ -1095,7 +1237,7 @@ public abstract class MutableOfferView> exten totalToPayTextField = addFundsTextfield(gridPane, gridRow, Res.get("shared.totalsNeeded"), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); totalToPayTextField.setVisible(false); - GridPane.setMargin(totalToPayTextField, new Insets(65, 10, 0, 0)); + GridPane.setMargin(totalToPayTextField, new Insets(60 + heightAdjustment, 10, 0, 0)); qrCodeImageView = new ImageView(); qrCodeImageView.setVisible(false); @@ -1121,15 +1263,15 @@ public abstract class MutableOfferView> exten Res.get("shared.tradeWalletBalance")); balanceTextField.setVisible(false); - reserveExactAmountCheckbox = FormBuilder.addLabelCheckBox(gridPane, ++gridRow, - Res.get("shared.reserveExactAmount")); - - GridPane.setHalignment(reserveExactAmountCheckbox, HPos.LEFT); - - reserveExactAmountCheckbox.setVisible(false); - reserveExactAmountCheckbox.setSelected(preferences.getSplitOfferOutput()); - reserveExactAmountCheckbox.setOnAction(event -> { - boolean selected = reserveExactAmountCheckbox.isSelected(); + + reserveExactAmountSlider = FormBuilder.addSlideToggleButton(gridPane, ++gridRow, Res.get("shared.reserveExactAmount"), heightAdjustment); + GridPane.setHalignment(reserveExactAmountSlider, HPos.LEFT); + GridPane.setMargin(reserveExactAmountSlider, new Insets(-5, 0, -5, 0)); + reserveExactAmountSlider.setPadding(new Insets(0, 0, 0, 0)); + reserveExactAmountSlider.setVisible(false); + reserveExactAmountSlider.setSelected(preferences.getSplitOfferOutput()); + reserveExactAmountSlider.setOnAction(event -> { + boolean selected = reserveExactAmountSlider.isSelected(); if (selected != preferences.getSplitOfferOutput()) { preferences.setSplitOfferOutput(selected); model.dataModel.setReserveExactAmount(selected); @@ -1211,6 +1353,8 @@ public abstract class MutableOfferView> exten }); cancelButton2.setDefaultButton(false); cancelButton2.setVisible(false); + + hideFundingGroup(); } private void openWallet() { @@ -1289,7 +1433,7 @@ public abstract class MutableOfferView> exten firstRowHBox.getChildren().addAll(amountBox, xLabel, percentagePriceBox, resultLabel, volumeBox); GridPane.setColumnSpan(firstRowHBox, 2); GridPane.setRowIndex(firstRowHBox, gridRow); - GridPane.setMargin(firstRowHBox, new Insets(40, 10, 0, 0)); + GridPane.setMargin(firstRowHBox, new Insets(40 + heightAdjustment, 10, 0, 0)); gridPane.getChildren().add(firstRowHBox); } 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 3323308332..821f9e8a5c 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; @@ -39,7 +41,6 @@ import haveno.core.offer.OfferUtil; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; -import haveno.core.payment.payload.PaymentMethod; import haveno.core.payment.validation.FiatVolumeValidator; import haveno.core.payment.validation.SecurityDepositValidator; import haveno.core.payment.validation.XmrValidator; @@ -56,6 +57,7 @@ import haveno.core.util.coin.CoinUtil; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; import haveno.core.util.validation.InputValidator; +import haveno.core.util.validation.InputValidator.ValidationResult; import haveno.core.util.validation.MonetaryValidator; import haveno.core.xmr.wallet.Restrictions; import haveno.desktop.Navigation; @@ -99,7 +101,7 @@ public abstract class MutableOfferViewModel ext private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final Preferences preferences; - protected final CoinFormatter btcFormatter; + protected final CoinFormatter xmrFormatter; private final FiatVolumeValidator fiatVolumeValidator; private final AmountValidator4Decimals amountValidator4Decimals; private final AmountValidator8Decimals amountValidator8Decimals; @@ -108,14 +110,14 @@ 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(); public final StringProperty minAmount = new SimpleStringProperty(); - protected final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositInBTC = new SimpleStringProperty(); - final StringProperty buyerSecurityDepositLabel = new SimpleStringProperty(); + protected final StringProperty securityDeposit = new SimpleStringProperty(); + final StringProperty securityDepositInXMR = new SimpleStringProperty(); + final StringProperty securityDepositLabel = new SimpleStringProperty(); // Price in the viewModel is always dependent on fiat/crypto: Fiat Fiat/BTC, for cryptos we use inverted price. // The domain (dataModel) uses always the same price model (otherCurrencyBTC) @@ -143,6 +145,7 @@ public abstract class MutableOfferViewModel ext final StringProperty waitingForFundsText = new SimpleStringProperty(""); final StringProperty triggerPriceDescription = new SimpleStringProperty(""); final StringProperty percentagePriceDescription = new SimpleStringProperty(""); + final StringProperty extraInfo = new SimpleStringProperty(""); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); @@ -151,26 +154,29 @@ public abstract class MutableOfferViewModel ext final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - final BooleanProperty isMinBuyerSecurityDeposit = new SimpleBooleanProperty(); + final BooleanProperty isMinSecurityDeposit = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty extraInfoValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; private ChangeListener priceStringListener, marketPriceMarginStringListener; private ChangeListener volumeStringListener; private ChangeListener securityDepositStringListener; + private ChangeListener extraInfoStringListener; private ChangeListener amountListener; private ChangeListener minAmountListener; private ChangeListener priceListener; private ChangeListener volumeListener; private ChangeListener securityDepositAsDoubleListener; + private ChangeListener buyerAsTakerWithoutDepositListener; private ChangeListener isWalletFundedListener; private ChangeListener errorMessageListener; @@ -182,7 +188,6 @@ public abstract class MutableOfferViewModel ext final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); private ChangeListener currenciesUpdateListener; protected boolean syncMinAmountWithAmount = true; - private boolean makeOfferFromUnsignedAccountWarningDisplayed; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -193,26 +198,26 @@ public abstract class MutableOfferViewModel ext FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals amountValidator4Decimals, AmountValidator8Decimals amountValidator8Decimals, - XmrValidator btcValidator, + XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; - this.xmrValidator = btcValidator; + this.xmrValidator = xmrValidator; this.securityDepositValidator = securityDepositValidator; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.preferences = preferences; - this.btcFormatter = btcFormatter; + this.xmrFormatter = xmrFormatter; this.offerUtil = offerUtil; paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); @@ -236,6 +241,7 @@ public abstract class MutableOfferViewModel ext dataModel.calculateTotalToPay(); updateButtonDisableState(); updateSpinnerInfo(); + setExtraInfoToModel(); }, 100, TimeUnit.MILLISECONDS); } @@ -268,7 +274,7 @@ public abstract class MutableOfferViewModel ext dataModel.getTradeCurrencyCode())); } volumePromptLabel.bind(createStringBinding( - () -> Res.get("createOffer.volume.prompt", dataModel.getTradeCurrencyCode().get()), + () -> Res.get("createOffer.volume.prompt", CurrencyUtil.getCurrencyCodeBase(dataModel.getTradeCurrencyCode().get())), dataModel.getTradeCurrencyCode())); totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true), @@ -303,7 +309,7 @@ public abstract class MutableOfferViewModel ext dataModel.calculateVolume(); dataModel.calculateTotalToPay(); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); updateButtonDisableState(); } }; @@ -419,34 +425,36 @@ public abstract class MutableOfferViewModel ext updateButtonDisableState(); } }; + securityDepositStringListener = (ov, oldValue, newValue) -> { if (!ignoreSecurityDepositStringListener) { if (securityDepositValidator.validate(newValue).isValid) { - setBuyerSecurityDepositToModel(); + setSecurityDepositToModel(); dataModel.calculateTotalToPay(); } updateButtonDisableState(); } }; - amountListener = (ov, oldValue, newValue) -> { if (newValue != null) { amount.set(HavenoUtils.formatXmr(newValue)); - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } else { amount.set(""); - buyerSecurityDepositInBTC.set(""); + securityDepositInXMR.set(""); } applyMakerFee(); }; + minAmountListener = (ov, oldValue, newValue) -> { if (newValue != null) minAmount.set(HavenoUtils.formatXmr(newValue)); else minAmount.set(""); }; + priceListener = (ov, oldValue, newValue) -> { ignorePriceStringListener = true; if (newValue != null) @@ -457,6 +465,7 @@ public abstract class MutableOfferViewModel ext ignorePriceStringListener = false; applyMakerFee(); }; + volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) @@ -470,17 +479,35 @@ public abstract class MutableOfferViewModel ext securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { - buyerSecurityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); + securityDeposit.set(FormattingUtils.formatToPercent((double) newValue)); if (dataModel.getAmount().get() != null) { - buyerSecurityDepositInBTC.set(HavenoUtils.formatXmr(dataModel.getBuyerSecurityDeposit(), true)); + securityDepositInXMR.set(HavenoUtils.formatXmr(dataModel.getSecurityDeposit(), true)); } - updateBuyerSecurityDeposit(); + updateSecurityDeposit(); } else { - buyerSecurityDeposit.set(""); - buyerSecurityDepositInBTC.set(""); + securityDeposit.set(""); + securityDepositInXMR.set(""); } }; + buyerAsTakerWithoutDepositListener = (ov, oldValue, newValue) -> { + if (dataModel.paymentAccount != null) xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); + if (amount.get() != null) amountValidationResult.set(isXmrInputValid(amount.get())); + updateSecurityDeposit(); + setSecurityDepositToModel(); + onFocusOutSecurityDepositTextField(true, false); // refresh security deposit field + applyMakerFee(); + dataModel.calculateTotalToPay(); + updateButtonDisableState(); + }; + + extraInfoStringListener = (ov, oldValue, newValue) -> { + if (newValue != null) { + extraInfo.set(newValue); + UserThread.execute(() -> onExtraInfoTextAreaChanged()); + } + }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { @@ -506,7 +533,7 @@ public abstract class MutableOfferViewModel ext tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), - btcFormatter)); + xmrFormatter)); } @@ -525,14 +552,17 @@ public abstract class MutableOfferViewModel ext marketPriceMargin.addListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); - buyerSecurityDeposit.addListener(securityDepositStringListener); + securityDeposit.addListener(securityDepositStringListener); + extraInfo.addListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); dataModel.getMinAmount().addListener(minAmountListener); dataModel.getPrice().addListener(priceListener); dataModel.getVolume().addListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().addListener(buyerAsTakerWithoutDepositListener); + dataModel.getExtraInfo().addListener(extraInfoStringListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); @@ -547,14 +577,17 @@ public abstract class MutableOfferViewModel ext marketPriceMargin.removeListener(marketPriceMarginStringListener); dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); - buyerSecurityDeposit.removeListener(securityDepositStringListener); + securityDeposit.removeListener(securityDepositStringListener); + extraInfo.removeListener(extraInfoStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); dataModel.getMinAmount().removeListener(minAmountListener); dataModel.getPrice().removeListener(priceListener); dataModel.getVolume().removeListener(volumeListener); - dataModel.getBuyerSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); + dataModel.getExtraInfo().removeListener(extraInfoStringListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -570,8 +603,8 @@ public abstract class MutableOfferViewModel ext // API /////////////////////////////////////////////////////////////////////////////////////////// - boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { - boolean result = dataModel.initWithData(direction, tradeCurrency); + boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) { + boolean result = dataModel.initWithData(direction, tradeCurrency, initAddressEntry); if (dataModel.getAddressEntry() != null) { addressAsString = dataModel.getAddressEntry().getAddressString(); } @@ -593,9 +626,9 @@ public abstract class MutableOfferViewModel ext } securityDepositValidator.setPaymentAccount(dataModel.paymentAccount); - validateAndSetBuyerSecurityDepositToModel(); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); + validateAndSetSecurityDepositToModel(); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); + securityDepositLabel.set(getSecurityDepositLabel()); applyMakerFee(); return result; @@ -607,35 +640,40 @@ public abstract class MutableOfferViewModel ext /////////////////////////////////////////////////////////////////////////////////////////// void onPlaceOffer(Offer offer, Runnable resultHandler) { - errorMessage.set(null); - createOfferRequested = true; - createOfferCanceled = false; - - dataModel.onPlaceOffer(offer, transaction -> { - resultHandler.run(); - 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.getOpenOfferById(offer.getId()); + Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); if (openOffer.isPresent()) { openOfferManager.cancelOpenOffer(openOffer.get(), () -> { UserThread.execute(() -> { @@ -663,7 +701,6 @@ public abstract class MutableOfferViewModel ext xmrValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit(dataModel.getTradeCurrencyCode().get())); xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); - maybeShowMakeOfferToUnsignedAccountWarning(); securityDepositValidator.setPaymentAccount(paymentAccount); } @@ -754,7 +791,8 @@ public abstract class MutableOfferViewModel ext } } } - // We want to trigger a recalculation of the volume + + // trigger recalculation of the volume UserThread.execute(() -> { onFocusOutVolumeTextField(true, false); onFocusOutMinAmountTextField(true, false); @@ -799,7 +837,24 @@ public abstract class MutableOfferViewModel ext syncMinAmountWithAmount = true; } - maybeShowMakeOfferToUnsignedAccountWarning(); + // trigger recalculation of the security deposit + UserThread.execute(() -> { + onFocusOutSecurityDepositTextField(true, false); + }); + } + } + + public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + onExtraInfoTextAreaChanged(); + } + } + + public void onExtraInfoTextAreaChanged() { + extraInfoValidationResult.set(getExtraInfoValidationResult()); + updateButtonDisableState(); + if (extraInfoValidationResult.get().isValid) { + setExtraInfoToModel(); } } @@ -929,17 +984,22 @@ public abstract class MutableOfferViewModel ext if (marketPriceMargin.get() == null && amount.get() != null && volume.get() != null) { updateMarketPriceToManual(); } + + // trigger recalculation of security deposit + UserThread.execute(() -> { + onFocusOutSecurityDepositTextField(true, false); + }); } } - void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = securityDepositValidator.validate(buyerSecurityDeposit.get()); - buyerSecurityDepositValidationResult.set(result); + void onFocusOutSecurityDepositTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue && !isMinSecurityDeposit.get()) { + InputValidator.ValidationResult result = securityDepositValidator.validate(securityDeposit.get()); + securityDepositValidationResult.set(result); if (result.isValid) { - double defaultSecurityDeposit = Restrictions.getDefaultBuyerSecurityDepositAsPercent(); + double defaultSecurityDeposit = Restrictions.getDefaultSecurityDepositAsPercent(); String key = "buyerSecurityDepositIsLowerAsDefault"; - double depositAsDouble = ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()); + double depositAsDouble = ParsingUtils.parsePercentStringToDouble(securityDeposit.get()); if (preferences.showAgain(key) && depositAsDouble < defaultSecurityDeposit) { String postfix = dataModel.isBuyOffer() ? Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : @@ -950,26 +1010,26 @@ public abstract class MutableOfferViewModel ext .width(800) .actionButtonText(Res.get("createOffer.resetToDefault")) .onAction(() -> { - dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit); + dataModel.setSecurityDepositPct(defaultSecurityDeposit); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; }) .closeButtonText(Res.get("createOffer.useLowerValue")) - .onClose(this::applyBuyerSecurityDepositOnFocusOut) + .onClose(this::applySecurityDepositOnFocusOut) .dontShowAgainId(key) .show(); } else { - applyBuyerSecurityDepositOnFocusOut(); + applySecurityDepositOnFocusOut(); } } } } - private void applyBuyerSecurityDepositOnFocusOut() { - setBuyerSecurityDepositToModel(); + private void applySecurityDepositOnFocusOut() { + setSecurityDepositToModel(); ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + securityDeposit.set(FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get())); ignoreSecurityDepositStringListener = false; } @@ -1001,8 +1061,8 @@ public abstract class MutableOfferViewModel ext .show(); } - CoinFormatter getBtcFormatter() { - return btcFormatter; + CoinFormatter getXmrFormatter() { + return xmrFormatter; } public boolean isShownAsBuyOffer() { @@ -1020,24 +1080,27 @@ public abstract class MutableOfferViewModel ext public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getSecurityDepositLabel() { - return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDeposit") : + dataModel.isMinSecurityDeposit() ? Res.get("createOffer.minSecurityDepositUsed") : + Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") : dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit"); } - public String getSecurityDepositPopOverLabel(String depositInBTC) { - return dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInBTC) : - Res.get("createOffer.securityDepositInfo", depositInBTC); + public String getSecurityDepositPopOverLabel(String depositInXMR) { + return dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer() ? Res.get("createOffer.myDepositInfo", depositInXMR) : + dataModel.isBuyOffer() ? Res.get("createOffer.securityDepositInfoAsBuyer", depositInXMR) : + Res.get("createOffer.securityDepositInfo", depositInXMR); } public String getSecurityDepositInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter + xmrFormatter ); } @@ -1050,7 +1113,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getMakerFeePercentage() { @@ -1061,7 +1124,7 @@ public abstract class MutableOfferViewModel ext public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), - btcFormatter); + xmrFormatter); } public String getFundsStructure() { @@ -1134,7 +1197,7 @@ public abstract class MutableOfferViewModel ext private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { - BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), btcFormatter)); + BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); @@ -1155,7 +1218,7 @@ public abstract class MutableOfferViewModel ext private void setMinAmountToModel() { if (minAmount.get() != null && !minAmount.get().isEmpty()) { - BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), btcFormatter)); + BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); @@ -1193,30 +1256,27 @@ public abstract class MutableOfferViewModel ext } } - private void setBuyerSecurityDepositToModel() { - if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) { - dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get())); + private void setSecurityDepositToModel() { + if (securityDeposit.get() != null && !securityDeposit.get().isEmpty() && !isMinSecurityDeposit.get()) { + dataModel.setSecurityDepositPct(ParsingUtils.parsePercentStringToDouble(securityDeposit.get())); } else { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } - private void validateAndSetBuyerSecurityDepositToModel() { + private void setExtraInfoToModel() { + if (extraInfo.get() != null && !extraInfo.get().isEmpty()) { + dataModel.setExtraInfo(extraInfo.get()); + } else { + dataModel.setExtraInfo(null); + } + } + + private void validateAndSetSecurityDepositToModel() { // If the security deposit in the model is not valid percent - String value = FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get()); + String value = FormattingUtils.formatToPercent(dataModel.getSecurityDepositPct().get()); if (!securityDepositValidator.validate(value).isValid) { - dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); - } - } - - private void maybeShowMakeOfferToUnsignedAccountWarning() { - if (!makeOfferFromUnsignedAccountWarningDisplayed && - dataModel.getDirection() == OfferDirection.SELL && - PaymentMethod.hasChargebackRisk(dataModel.getPaymentAccount().getPaymentMethod(), dataModel.getTradeCurrency().getCode())) { - BigInteger checkAmount = dataModel.getMinAmount().get() == null ? dataModel.getAmount().get() : dataModel.getMinAmount().get(); - if (checkAmount != null && checkAmount.compareTo(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT) <= 0) { - makeOfferFromUnsignedAccountWarningDisplayed = true; - } + dataModel.setSecurityDepositPct(Restrictions.getDefaultSecurityDepositAsPercent()); } } @@ -1263,15 +1323,17 @@ public abstract class MutableOfferViewModel ext isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); } - private void updateBuyerSecurityDeposit() { - isMinBuyerSecurityDeposit.set(dataModel.isMinBuyerSecurityDeposit()); - - if (dataModel.isMinBuyerSecurityDeposit()) { - buyerSecurityDepositLabel.set(Res.get("createOffer.minSecurityDepositUsed")); - buyerSecurityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinBuyerSecurityDeposit())); + private void updateSecurityDeposit() { + isMinSecurityDeposit.set(dataModel.isMinSecurityDeposit()); + securityDepositLabel.set(getSecurityDepositLabel()); + if (dataModel.isMinSecurityDeposit()) { + securityDeposit.set(HavenoUtils.formatXmr(Restrictions.getMinSecurityDeposit())); + securityDepositValidationResult.set(new ValidationResult(true)); } else { - buyerSecurityDepositLabel.set(getSecurityDepositLabel()); - buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDepositPct().get())); + boolean hasBuyerAsTakerWithoutDeposit = dataModel.buyerAsTakerWithoutDeposit.get() && dataModel.isSellOffer(); + securityDeposit.set(FormattingUtils.formatToPercent(hasBuyerAsTakerWithoutDeposit ? + Restrictions.getDefaultSecurityDepositAsPercent() : // use default percent if no deposit from buyer + dataModel.getSecurityDepositPct().get())); } } @@ -1293,12 +1355,22 @@ public abstract class MutableOfferViewModel ext } // validating the percentage deposit value only makes sense if it is actually used - if (!dataModel.isMinBuyerSecurityDeposit()) { - inputDataValid = inputDataValid && securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + if (!dataModel.isMinSecurityDeposit()) { + inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } + 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)); + } else { + return new InputValidator.ValidationResult(true); + } } private void updateMarketPriceToManual() { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java index 533f2834e4..71a827ffa3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferDataModel.java @@ -65,29 +65,7 @@ public abstract class OfferDataModel extends ActivatableDataModel { this.offerUtil = offerUtil; } - protected void updateBalance() { - updateBalances(); - UserThread.await(() -> { - missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); - isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), balance.get())); - if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { - showWalletFundedNotification.set(true); - } - }); - } - - protected void updateAvailableBalance() { - updateBalances(); - UserThread.await(() -> { - missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), availableBalance.get())); - isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), availableBalance.get())); - if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { - showWalletFundedNotification.set(true); - } - }); - } - - private void updateBalances() { + protected void updateBalances() { BigInteger tradeWalletBalance = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger tradeWalletAvailableBalance = xmrWalletService.getAvailableBalanceForSubaddress(addressEntry.getSubaddressIndex()); BigInteger walletBalance = xmrWalletService.getBalance(); @@ -101,10 +79,16 @@ public abstract class OfferDataModel extends ActivatableDataModel { availableBalance.set(totalToPay.get().min(totalAvailableBalance)); } } else { + totalBalance = tradeWalletBalance; + totalAvailableBalance = tradeWalletAvailableBalance; balance.set(tradeWalletBalance); availableBalance.set(tradeWalletAvailableBalance); } }); } + + public boolean hasTotalToPay() { + return totalToPay.get() != null && totalToPay.get().compareTo(BigInteger.ZERO) > 0; + } } 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 6ba827f09c..6f6aba7cbc 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferView.java @@ -33,14 +33,15 @@ import haveno.desktop.common.view.View; import haveno.desktop.common.view.ViewLoader; import haveno.desktop.main.MainView; import haveno.desktop.main.offer.createoffer.CreateOfferView; -import haveno.desktop.main.offer.offerbook.XmrOfferBookView; +import haveno.desktop.main.offer.offerbook.FiatOfferBookView; import haveno.desktop.main.offer.offerbook.OfferBookView; +import haveno.desktop.main.offer.offerbook.CryptoOfferBookView; import haveno.desktop.main.offer.offerbook.OtherOfferBookView; -import haveno.desktop.main.offer.offerbook.TopCryptoOfferBookView; import haveno.desktop.main.offer.takeoffer.TakeOfferView; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import javafx.beans.value.ChangeListener; +import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import org.jetbrains.annotations.NotNull; @@ -50,9 +51,9 @@ import java.util.Optional; public abstract class OfferView extends ActivatableView { - private OfferBookView xmrOfferBookView, topCryptoOfferBookView, otherOfferBookView; + private OfferBookView fiatOfferBookView, cryptoOfferBookView, otherOfferBookView; - private Tab xmrOfferBookTab, topCryptoOfferBookTab, otherOfferBookTab; + private Tab labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab; private final ViewLoader viewLoader; private final Navigation navigation; @@ -95,17 +96,17 @@ public abstract class OfferView extends ActivatableView { tabChangeListener = (observableValue, oldValue, newValue) -> { UserThread.execute(() -> { if (newValue != null) { - if (newValue.equals(xmrOfferBookTab)) { - if (xmrOfferBookView != null) { - xmrOfferBookView.onTabSelected(true); + if (newValue.equals(fiatOfferBookTab)) { + if (fiatOfferBookView != null) { + fiatOfferBookView.onTabSelected(true); } else { - loadView(XmrOfferBookView.class, null, null); + loadView(FiatOfferBookView.class, null, null); } - } else if (newValue.equals(topCryptoOfferBookTab)) { - if (topCryptoOfferBookView != null) { - topCryptoOfferBookView.onTabSelected(true); + } else if (newValue.equals(cryptoOfferBookTab)) { + if (cryptoOfferBookView != null) { + cryptoOfferBookView.onTabSelected(true); } else { - loadView(TopCryptoOfferBookView.class, null, null); + loadView(CryptoOfferBookView.class, null, null); } } else if (newValue.equals(otherOfferBookTab)) { if (otherOfferBookView != null) { @@ -116,10 +117,10 @@ public abstract class OfferView extends ActivatableView { } } if (oldValue != null) { - if (oldValue.equals(xmrOfferBookTab) && xmrOfferBookView != null) { - xmrOfferBookView.onTabSelected(false); - } else if (oldValue.equals(topCryptoOfferBookTab) && topCryptoOfferBookView != null) { - topCryptoOfferBookView.onTabSelected(false); + if (oldValue.equals(fiatOfferBookTab) && fiatOfferBookView != null) { + fiatOfferBookView.onTabSelected(false); + } else if (oldValue.equals(cryptoOfferBookTab) && cryptoOfferBookView != null) { + cryptoOfferBookView.onTabSelected(false); } else if (oldValue.equals(otherOfferBookTab) && otherOfferBookView != null) { otherOfferBookView.onTabSelected(false); } @@ -154,14 +155,8 @@ public abstract class OfferView extends ActivatableView { root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); navigation.addListener(navigationListener); - if (xmrOfferBookView == null) { - navigation.navigateTo(MainView.class, this.getClass(), XmrOfferBookView.class); - } - - GUIUtil.updateTopCrypto(preferences); - - if (topCryptoOfferBookTab != null) { - topCryptoOfferBookTab.setText(GUIUtil.TOP_CRYPTO.getName().toUpperCase()); + if (fiatOfferBookView == null) { + navigation.navigateTo(MainView.class, this.getClass(), FiatOfferBookView.class); } } @@ -171,6 +166,8 @@ public abstract class OfferView extends ActivatableView { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); } + protected abstract String getOfferLabel(); + private void loadView(Class viewClass, Class childViewClass, @Nullable Object data) { @@ -179,66 +176,75 @@ public abstract class OfferView extends ActivatableView { if (OfferBookView.class.isAssignableFrom(viewClass)) { - if (viewClass == XmrOfferBookView.class && xmrOfferBookTab != null && xmrOfferBookView != null) { + if (viewClass == FiatOfferBookView.class && fiatOfferBookTab != null && fiatOfferBookView != null) { if (childViewClass == null) { - xmrOfferBookTab.setContent(xmrOfferBookView.getRoot()); + fiatOfferBookTab.setContent(fiatOfferBookView.getRoot()); } else if (childViewClass == TakeOfferView.class) { - loadTakeViewClass(viewClass, childViewClass, xmrOfferBookTab); + loadTakeViewClass(viewClass, childViewClass, fiatOfferBookTab); } else { - loadCreateViewClass(xmrOfferBookView, viewClass, childViewClass, xmrOfferBookTab, (PaymentMethod) data); + loadCreateViewClass(fiatOfferBookView, viewClass, childViewClass, fiatOfferBookTab, (PaymentMethod) data); } - tabPane.getSelectionModel().select(xmrOfferBookTab); - } else if (viewClass == TopCryptoOfferBookView.class && topCryptoOfferBookTab != null && topCryptoOfferBookView != null) { + tabPane.getSelectionModel().select(fiatOfferBookTab); + } else if (viewClass == CryptoOfferBookView.class && cryptoOfferBookTab != null && cryptoOfferBookView != null) { if (childViewClass == null) { - topCryptoOfferBookTab.setContent(topCryptoOfferBookView.getRoot()); + cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot()); } else if (childViewClass == TakeOfferView.class) { - loadTakeViewClass(viewClass, childViewClass, topCryptoOfferBookTab); + loadTakeViewClass(viewClass, childViewClass, cryptoOfferBookTab); } else { - tradeCurrency = GUIUtil.TOP_CRYPTO; - loadCreateViewClass(topCryptoOfferBookView, viewClass, childViewClass, topCryptoOfferBookTab, (PaymentMethod) data); - } - tabPane.getSelectionModel().select(topCryptoOfferBookTab); - } else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) { - if (childViewClass == null) { - otherOfferBookTab.setContent(otherOfferBookView.getRoot()); - } else if (childViewClass == TakeOfferView.class) { - loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab); - } else { - //add sanity check in case of app restart + // 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(); } + loadCreateViewClass(cryptoOfferBookView, viewClass, childViewClass, cryptoOfferBookTab, (PaymentMethod) data); + } + tabPane.getSelectionModel().select(cryptoOfferBookTab); + } else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) { + if (childViewClass == null) { + otherOfferBookTab.setContent(otherOfferBookView.getRoot()); + } else if (childViewClass == TakeOfferView.class) { + loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab); + } else { loadCreateViewClass(otherOfferBookView, viewClass, childViewClass, otherOfferBookTab, (PaymentMethod) data); } tabPane.getSelectionModel().select(otherOfferBookTab); } else { - if (xmrOfferBookTab == null) { - xmrOfferBookTab = new Tab(Res.getBaseCurrencyName().toUpperCase()); - xmrOfferBookTab.setClosable(false); - topCryptoOfferBookTab = new Tab(GUIUtil.TOP_CRYPTO.getName().toUpperCase()); - topCryptoOfferBookTab.setClosable(false); + if (fiatOfferBookTab == null) { + + // add preceding label tab + labelTab = new Tab(); + labelTab.setDisable(true); + labelTab.setContent(new Label()); + 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;"); + labelTab.setGraphic(offerLabel); + + fiatOfferBookTab = new Tab(Res.get("shared.fiat").toUpperCase()); + fiatOfferBookTab.setClosable(false); + cryptoOfferBookTab = new Tab(Res.get("shared.crypto").toUpperCase()); + cryptoOfferBookTab.setClosable(false); otherOfferBookTab = new Tab(Res.get("shared.other").toUpperCase()); otherOfferBookTab.setClosable(false); - - tabPane.getTabs().addAll(xmrOfferBookTab, topCryptoOfferBookTab, otherOfferBookTab); + tabPane.getTabs().addAll(labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab); } - if (viewClass == XmrOfferBookView.class) { - xmrOfferBookView = (XmrOfferBookView) viewLoader.load(XmrOfferBookView.class); - xmrOfferBookView.setOfferActionHandler(offerActionHandler); - xmrOfferBookView.setDirection(direction); - xmrOfferBookView.onTabSelected(true); - tabPane.getSelectionModel().select(xmrOfferBookTab); - xmrOfferBookTab.setContent(xmrOfferBookView.getRoot()); - } else if (viewClass == TopCryptoOfferBookView.class) { - topCryptoOfferBookView = (TopCryptoOfferBookView) viewLoader.load(TopCryptoOfferBookView.class); - topCryptoOfferBookView.setOfferActionHandler(offerActionHandler); - topCryptoOfferBookView.setDirection(direction); - topCryptoOfferBookView.onTabSelected(true); - tabPane.getSelectionModel().select(topCryptoOfferBookTab); - topCryptoOfferBookTab.setContent(topCryptoOfferBookView.getRoot()); + if (viewClass == FiatOfferBookView.class) { + fiatOfferBookView = (FiatOfferBookView) viewLoader.load(FiatOfferBookView.class); + fiatOfferBookView.setOfferActionHandler(offerActionHandler); + fiatOfferBookView.setDirection(direction); + fiatOfferBookView.onTabSelected(true); + tabPane.getSelectionModel().select(fiatOfferBookTab); + fiatOfferBookTab.setContent(fiatOfferBookView.getRoot()); + } else if (viewClass == CryptoOfferBookView.class) { + cryptoOfferBookView = (CryptoOfferBookView) viewLoader.load(CryptoOfferBookView.class); + cryptoOfferBookView.setOfferActionHandler(offerActionHandler); + cryptoOfferBookView.setDirection(direction); + cryptoOfferBookView.onTabSelected(true); + tabPane.getSelectionModel().select(cryptoOfferBookTab); + cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot()); } else if (viewClass == OtherOfferBookView.class) { otherOfferBookView = (OtherOfferBookView) viewLoader.load(OtherOfferBookView.class); otherOfferBookView.setOfferActionHandler(offerActionHandler); @@ -265,11 +271,7 @@ public abstract class OfferView extends ActivatableView { // in different graphs view = viewLoader.load(childViewClass); - // Invert direction for non-Fiat trade currencies -> BUY BCH is to SELL Monero - OfferDirection offerDirection = CurrencyUtil.isFiatCurrency(tradeCurrency.getCode()) ? direction : - direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; - - ((CreateOfferView) view).initWithData(offerDirection, tradeCurrency, offerActionHandler); + ((CreateOfferView) view).initWithData(direction, tradeCurrency, offerActionHandler); ((SelectableView) view).onTabSelected(true); @@ -329,9 +331,9 @@ public abstract class OfferView extends ActivatableView { private Class> getOfferBookViewClassFor(String currencyCode) { Class> offerBookViewClass; if (CurrencyUtil.isFiatCurrency(currencyCode)) { - offerBookViewClass = XmrOfferBookView.class; - } else if (currencyCode.equals(GUIUtil.TOP_CRYPTO.getCode())) { - offerBookViewClass = TopCryptoOfferBookView.class; + offerBookViewClass = FiatOfferBookView.class; + } else if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + offerBookViewClass = CryptoOfferBookView.class; } else { offerBookViewClass = OtherOfferBookView.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 4f18294d93..a01f2f3e96 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/OfferViewUtil.java @@ -22,16 +22,16 @@ import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; import haveno.core.locale.TradeCurrency; +import haveno.core.locale.TraditionalCurrency; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.components.AutoTooltipLabel; -import haveno.desktop.main.offer.offerbook.XmrOfferBookView; +import haveno.desktop.main.offer.offerbook.FiatOfferBookView; import haveno.desktop.main.offer.offerbook.OfferBookView; +import haveno.desktop.main.offer.offerbook.CryptoOfferBookView; import haveno.desktop.main.offer.offerbook.OtherOfferBookView; -import haveno.desktop.main.offer.offerbook.TopCryptoOfferBookView; import haveno.desktop.main.overlays.popups.Popup; -import haveno.desktop.util.GUIUtil; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.VPos; @@ -44,7 +44,6 @@ import monero.daemon.model.MoneroSubmitTxResult; import org.jetbrains.annotations.NotNull; import java.util.HashMap; -import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -90,10 +89,10 @@ public class OfferViewUtil { public static Class> getOfferBookViewClass(String currencyCode) { Class> offerBookViewClazz; - if (CurrencyUtil.isTraditionalCurrency(currencyCode)) { - offerBookViewClazz = XmrOfferBookView.class; - } else if (currencyCode.equals(GUIUtil.TOP_CRYPTO.getCode())) { - offerBookViewClazz = TopCryptoOfferBookView.class; + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + offerBookViewClazz = FiatOfferBookView.class; + } else if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + offerBookViewClazz = CryptoOfferBookView.class; } else { offerBookViewClazz = OtherOfferBookView.class; } @@ -109,7 +108,7 @@ public class OfferViewUtil { } public static boolean isShownAsSellOffer(String currencyCode, OfferDirection direction) { - return CurrencyUtil.isFiatCurrency(currencyCode) == (direction == OfferDirection.SELL); + return direction == OfferDirection.SELL; } public static boolean isShownAsBuyOffer(Offer offer) { @@ -124,10 +123,18 @@ public class OfferViewUtil { return getMainCryptoCurrencies().findAny().get(); } + public static TradeCurrency getAnyOfOtherCurrencies() { + return getOtherCurrencies().findAny().get(); + } + @NotNull public static Stream getMainCryptoCurrencies() { - return CurrencyUtil.getMainCryptoCurrencies().stream().filter(cryptoCurrency -> - !Objects.equals(cryptoCurrency.getCode(), GUIUtil.TOP_CRYPTO.getCode())); + return CurrencyUtil.getMainCryptoCurrencies().stream(); + } + + @NotNull + public static Stream getOtherCurrencies() { + return CurrencyUtil.getTraditionalNonFiatCurrencies().stream(); } public static void submitTransactionHex(XmrWalletService xmrWalletService, diff --git a/desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.java index 931f70ac66..fde0e57f02 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/SellOfferView.java @@ -18,6 +18,8 @@ package haveno.desktop.main.offer; import com.google.inject.Inject; + +import haveno.core.locale.Res; import haveno.core.offer.OfferDirection; import haveno.core.user.Preferences; import haveno.core.user.User; @@ -42,4 +44,9 @@ public class SellOfferView extends OfferView { p2PService, OfferDirection.SELL); } + + @Override + protected String getOfferLabel() { + return Res.get("offerbook.sellXmrFor"); + } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java index e30d53fdc1..f88bed1808 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/createoffer/CreateOfferView.java @@ -31,8 +31,6 @@ import haveno.desktop.common.view.FxmlView; import haveno.desktop.main.offer.MutableOfferView; import haveno.desktop.main.offer.OfferView; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; -import haveno.desktop.util.GUIUtil; -import java.util.Objects; import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -49,23 +47,22 @@ public class CreateOfferView extends MutableOfferView { super(model, navigation, preferences, offerDetailsWindow, btcFormatter); } - @Override public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency, OfferView.OfferActionHandler offerActionHandler) { - super.initWithData(direction, tradeCurrency, offerActionHandler); + super.initWithData(direction, tradeCurrency, true, offerActionHandler); } @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { return FXCollections.observableArrayList( paymentAccounts.stream().filter(paymentAccount -> { - if (model.getTradeCurrency().equals(GUIUtil.TOP_CRYPTO)) { - return Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_CRYPTO); - } else if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) { + if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) { return paymentAccount.isFiat(); + } else if (CurrencyUtil.isCryptoCurrency(model.getTradeCurrency().getCode())) { + return paymentAccount.isCryptoCurrency(); } else { - return !paymentAccount.isFiat() && !Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_CRYPTO); + return !paymentAccount.isFiat() && !paymentAccount.isCryptoCurrency(); } }).collect(Collectors.toList())); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.fxml b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.fxml similarity index 97% rename from desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.fxml rename to desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.fxml index 45a46ca7d0..658f138cf3 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.fxml @@ -19,7 +19,7 @@ - diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.java similarity index 56% rename from desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.java rename to desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.java index a6904d9635..8dfdc68671 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookView.java @@ -33,39 +33,29 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.scene.layout.GridPane; @FxmlView -public class TopCryptoOfferBookView extends OfferBookView { +public class CryptoOfferBookView extends OfferBookView { @Inject - TopCryptoOfferBookView(TopCryptoOfferBookViewModel model, - Navigation navigation, - OfferDetailsWindow offerDetailsWindow, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, - PrivateNotificationManager privateNotificationManager, - @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, - AccountAgeWitnessService accountAgeWitnessService, - SignedWitnessService signedWitnessService) { + CryptoOfferBookView(CryptoOfferBookViewModel model, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + PrivateNotificationManager privateNotificationManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService) { super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); } @Override protected String getMarketTitle() { return model.getDirection().equals(OfferDirection.BUY) ? - Res.get("offerbook.availableOffersToBuy", TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode(), Res.getBaseCurrencyCode()) : - Res.get("offerbook.availableOffersToSell", TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode(), Res.getBaseCurrencyCode()); - } - - @Override - protected void activate() { - model.onSetTradeCurrency(TopCryptoOfferBookViewModel.TOP_CRYPTO); - - super.activate(); - - currencyComboBoxContainer.setVisible(false); - currencyComboBoxContainer.setManaged(false); + Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.crypto")) : + Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.crypto")); } @Override String getTradeCurrencyCode() { - return TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode(); + return Res.getBaseCurrencyCode(); } } 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 new file mode 100644 index 0000000000..e97c08558e --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/CryptoOfferBookViewModel.java @@ -0,0 +1,148 @@ +/* + * 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.desktop.main.offer.offerbook; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.api.CoreApi; +import haveno.core.locale.CryptoCurrency; +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.GlobalSettings; +import haveno.core.locale.TradeCurrency; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OfferFilterService; +import haveno.core.offer.OpenOfferManager; +import haveno.core.payment.payload.PaymentMethod; +import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.ClosedTradableManager; +import haveno.core.user.Preferences; +import haveno.core.user.User; +import haveno.core.util.FormattingUtils; +import haveno.core.util.PriceUtil; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.xmr.setup.WalletsSetup; +import haveno.desktop.Navigation; +import haveno.desktop.main.offer.OfferViewUtil; +import haveno.desktop.util.GUIUtil; +import haveno.network.p2p.P2PService; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +public class CryptoOfferBookViewModel extends OfferBookViewModel { + + @Inject + public CryptoOfferBookViewModel(User user, + OpenOfferManager openOfferManager, + OfferBook offerBook, + Preferences preferences, + WalletsSetup walletsSetup, + P2PService p2PService, + PriceFeedService priceFeedService, + ClosedTradableManager closedTradableManager, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + PriceUtil priceUtil, + OfferFilterService offerFilterService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CoreApi coreApi) { + super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); + } + + @Override + void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { + if (direction == OfferDirection.BUY) { + preferences.setBuyScreenCryptoCurrencyCode(code); + } else { + preferences.setSellScreenCryptoCurrencyCode(code); + } + } + + @Override + protected ObservableList filterPaymentMethods(ObservableList list, + TradeCurrency selectedTradeCurrency) { + return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> { + return paymentMethod.isBlockchain(); + }).collect(Collectors.toList())); + } + + @Override + void fillCurrencies(ObservableList tradeCurrencies, + ObservableList allCurrencies) { + + tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + tradeCurrencies.addAll(preferences.getCryptoCurrenciesAsObservable().stream() + .collect(Collectors.toList())); + tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + + allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream() + .collect(Collectors.toList())); + allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + } + + @Override + Predicate getCurrencyAndMethodPredicate(OfferDirection direction, + TradeCurrency selectedTradeCurrency) { + 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()) && + (showAllTradeCurrenciesProperty.get() || + offer.getCurrencyCode().equals(selectedTradeCurrency.getCode())); + boolean paymentMethodResult = showAllPaymentMethods || + offer.getPaymentMethod().equals(selectedPaymentMethod); + boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); + return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; + }; + } + + @Override + TradeCurrency getDefaultTradeCurrency() { + TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); + + if (CurrencyUtil.isCryptoCurrency(defaultTradeCurrency.getCode()) && + hasPaymentAccountForCurrency(defaultTradeCurrency)) { + return defaultTradeCurrency; + } + + ObservableList tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies()); + if (!tradeCurrencies.isEmpty()) { + // drop show all entry and select first currency with payment account available + tradeCurrencies.remove(0); + List sortedList = tradeCurrencies.stream().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); + return sortedList.get(0); + } else { + return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) -> + Boolean.compare(!hasPaymentAccountForCurrency(o1), + !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); + } + } + + @Override + String getCurrencyCodeFromPreferences(OfferDirection direction) { + return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() : + preferences.getSellScreenCryptoCurrencyCode(); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.fxml b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.fxml similarity index 97% rename from desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.fxml rename to desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.fxml index e5bad7e7b0..2f2b6602cf 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.fxml @@ -19,7 +19,7 @@ - diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.java similarity index 75% rename from desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.java rename to desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.java index 07b4c5610d..9112ea9a80 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookView.java @@ -33,17 +33,17 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import javafx.scene.layout.GridPane; @FxmlView -public class XmrOfferBookView extends OfferBookView { +public class FiatOfferBookView extends OfferBookView { @Inject - XmrOfferBookView(XmrOfferBookViewModel model, - Navigation navigation, - OfferDetailsWindow offerDetailsWindow, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, - PrivateNotificationManager privateNotificationManager, - @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, - AccountAgeWitnessService accountAgeWitnessService, - SignedWitnessService signedWitnessService) { + FiatOfferBookView(FiatOfferBookViewModel model, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + PrivateNotificationManager privateNotificationManager, + @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService) { super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java similarity index 84% rename from desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookViewModel.java rename to desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java index 38702cde90..bea41743b6 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/XmrOfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/FiatOfferBookViewModel.java @@ -49,23 +49,23 @@ import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -public class XmrOfferBookViewModel extends OfferBookViewModel { +public class FiatOfferBookViewModel extends OfferBookViewModel { @Inject - public XmrOfferBookViewModel(User user, - OpenOfferManager openOfferManager, - OfferBook offerBook, - Preferences preferences, - WalletsSetup walletsSetup, - P2PService p2PService, - PriceFeedService priceFeedService, - ClosedTradableManager closedTradableManager, - AccountAgeWitnessService accountAgeWitnessService, - Navigation navigation, - PriceUtil priceUtil, - OfferFilterService offerFilterService, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - CoreApi coreApi) { + public FiatOfferBookViewModel(User user, + OpenOfferManager openOfferManager, + OfferBook offerBook, + Preferences preferences, + WalletsSetup walletsSetup, + P2PService p2PService, + PriceFeedService priceFeedService, + ClosedTradableManager closedTradableManager, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + PriceUtil priceUtil, + OfferFilterService offerFilterService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CoreApi coreApi) { super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); } @@ -141,9 +141,10 @@ public class XmrOfferBookViewModel extends OfferBookViewModel { !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); return sortedList.get(0); } else { - return CurrencyUtil.getMainTraditionalCurrencies().stream().sorted((o1, o2) -> - Boolean.compare(!hasPaymentAccountForCurrency(o1), - !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); + return CurrencyUtil.getMainTraditionalCurrencies().stream() + .filter(withFiatCurrency()) + .sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))) + .collect(Collectors.toList()).get(0); } } 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 2367f51c8d..2e92a9d42d 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 @@ -19,13 +19,12 @@ package haveno.desktop.main.offer.offerbook; import com.google.inject.Inject; import com.google.inject.Singleton; -import haveno.core.filter.FilterManager; + +import haveno.common.UserThread; import haveno.core.offer.Offer; import haveno.core.offer.OfferBookService; import static haveno.core.offer.OfferDirection.BUY; -import haveno.core.offer.OfferRestrictions; import haveno.network.p2p.storage.P2PDataStorage; -import haveno.network.utils.Utils; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,7 +48,6 @@ public class OfferBook { private final ObservableList offerBookListItems = FXCollections.observableArrayList(); private final Map buyOfferCountMap = new HashMap<>(); private final Map sellOfferCountMap = new HashMap<>(); - private final FilterManager filterManager; /////////////////////////////////////////////////////////////////////////////////////////// @@ -57,64 +55,47 @@ public class OfferBook { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - OfferBook(OfferBookService offerBookService, FilterManager filterManager) { + OfferBook(OfferBookService offerBookService) { this.offerBookService = offerBookService; - this.filterManager = filterManager; offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override public void onAdded(Offer offer) { - printOfferBookListItems("Before onAdded"); - // We get onAdded called every time a new ProtectedStorageEntry is received. - // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. - // We filter here to only add new offers if the same offer (using equals) was not already added and it - // is not banned. + UserThread.execute(() -> { + printOfferBookListItems("Before onAdded"); - if (filterManager.isOfferIdBanned(offer.getId())) { - log.debug("Ignored banned offer. ID={}", offer.getId()); - return; - } - - if (OfferRestrictions.requiresNodeAddressUpdate() && !Utils.isV3Address(offer.getMakerNodeAddress().getHostName())) { - log.debug("Ignored offer with Tor v2 node address. ID={}", offer.getId()); - return; - } - - // Use offer.equals(offer) to see if the OfferBook list contains an exact - // match -- offer.equals(offer) includes comparisons of payload, state - // and errorMessage. - synchronized (offerBookListItems) { - boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); - if (!hasSameOffer) { - OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); - removeDuplicateItem(newOfferBookListItem); - offerBookListItems.add(newOfferBookListItem); // Add replacement. - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("onAdded: Added new offer {}\n" - + "\twith newItem.payloadHash: {}", - offer.getId(), - newOfferBookListItem.hashOfPayload.getHex()); + // Use offer.equals(offer) to see if the OfferBook list contains an exact + // match -- offer.equals(offer) includes comparisons of payload, state + // and errorMessage. + synchronized (offerBookListItems) { + boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); + if (!hasSameOffer) { + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); + removeDuplicateItem(newOfferBookListItem); + offerBookListItems.add(newOfferBookListItem); // Add replacement. + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Added new offer {}\n" + + "\twith newItem.payloadHash: {}", + offer.getId(), + newOfferBookListItem.hashOfPayload.getHex()); + } + } else { + log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } - } else { - log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); + printOfferBookListItems("After onAdded"); } - printOfferBookListItems("After onAdded"); - } + }); } @Override public void onRemoved(Offer offer) { - synchronized (offerBookListItems) { - printOfferBookListItems("Before onRemoved"); - removeOffer(offer); - printOfferBookListItems("After onRemoved"); - } - } - }); - - filterManager.filterProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - // any notifications + UserThread.execute(() -> { + synchronized (offerBookListItems) { + printOfferBookListItems("Before onRemoved"); + removeOffer(offer); + printOfferBookListItems("After onRemoved"); + } + }); } }); } @@ -212,7 +193,6 @@ public class OfferBook { // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(this::isOfferAllowed) .map(OfferBookListItem::new) .collect(Collectors.toList())); @@ -248,13 +228,6 @@ public class OfferBook { return sellOfferCountMap; } - private boolean isOfferAllowed(Offer offer) { - boolean isBanned = filterManager.isOfferIdBanned(offer.getId()); - boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() - || Utils.isV3Address(offer.getMakerNodeAddress().getHostName()); - return !isBanned && isV3NodeAddressCompliant; - } - private void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); 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 3c0f1705ca..0248b9c522 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 @@ -17,8 +17,10 @@ package haveno.desktop.main.offer.offerbook; +import de.jensd.fx.fontawesome.AwesomeDude; 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.app.DevEnv; import haveno.common.util.Tuple3; @@ -44,7 +46,6 @@ import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.components.AccountStatusTooltipLabel; import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; -import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; @@ -52,7 +53,6 @@ import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.InfoAutoTooltipLabel; import haveno.desktop.components.PeerInfoIconTrading; -import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.content.cryptoaccounts.CryptoAccountsView; @@ -67,7 +67,6 @@ import haveno.desktop.main.portfolio.PortfolioView; import haveno.desktop.main.portfolio.editoffer.EditOfferView; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; -import haveno.desktop.util.Layout; import haveno.network.p2p.NodeAddress; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; @@ -85,6 +84,7 @@ import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; @@ -106,7 +106,6 @@ import java.util.Comparator; import java.util.Map; import java.util.Optional; -import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField; abstract public class OfferBookView extends ActivatableViewAndModel { @@ -119,12 +118,12 @@ abstract public class OfferBookView currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; private AutoTooltipTextField filterInputField; - private AutoTooltipSlideToggleButton matchingOffersToggle; + private ToggleButton matchingOffersToggleButton; + private ToggleButton noDepositOffersToggleButton; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; private AutoTooltipTableColumn marketColumn; @@ -170,18 +169,10 @@ abstract public class OfferBookView> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByCurrency")); @@ -195,14 +186,22 @@ abstract public class OfferBookView(); @@ -332,10 +331,6 @@ abstract public class OfferBookView offerCounts = OfferViewUtil.isShownAsBuyOffer(model.getDirection(), model.getSelectedTradeCurrency()) ? model.getSellOfferCounts() : model.getBuyOfferCounts(); currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactory(Res.get("shared.oneOffer"), @@ -362,11 +357,13 @@ abstract public class OfferBookView model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected())); + matchingOffersToggleButton.setSelected(model.useOffersMatchingMyAccountsFilter); + matchingOffersToggleButton.disableProperty().bind(model.disableMatchToggle); + matchingOffersToggleButton.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggleButton.isSelected())); + + noDepositOffersToggleButton.setSelected(model.showPrivateOffers); + noDepositOffersToggleButton.setOnAction(e -> model.onShowPrivateOffers(noDepositOffersToggleButton.isSelected())); - volumeColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not()); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); amountColumn.sortTypeProperty().addListener((observable, oldValue, newValue) -> { @@ -428,7 +425,7 @@ abstract public class OfferBookView { }); - tableView.setItems(model.getOfferList()); + UserThread.execute(() -> tableView.setItems(model.getOfferList())); model.getOfferList().addListener(offerListListener); nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())); @@ -469,8 +466,10 @@ abstract public class OfferBookView account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection()); + offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); new Popup() .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", HavenoUtils.formatXmr(tradeLimit, true), @@ -692,8 +695,13 @@ abstract public class OfferBookView getAmountColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.XMRMinMax"), Res.get("shared.amountHelp")); column.setMinWidth(100); + column.setSortable(true); column.getStyleClass().add("number-column"); column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); column.setCellFactory( @@ -815,7 +824,6 @@ abstract public class OfferBookView column = new AutoTooltipTableColumn<>("") { { setMinWidth(125); + setSortable(true); } }; column.getStyleClass().add("number-column"); @@ -1023,6 +1032,7 @@ abstract public class OfferBookView() { OfferFilterService.Result canTakeOfferResult = null; - final ImageView iconView = new ImageView(); - final AutoTooltipButton button = new AutoTooltipButton(); - - { - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setPrefWidth(10000); - } - - final ImageView iconView2 = new ImageView(); - final AutoTooltipButton button2 = new AutoTooltipButton(); - - { - button2.setGraphic(iconView2); - button2.setGraphicTextGap(10); - button2.setPrefWidth(10000); - } - - final HBox hbox = new HBox(); - - { - hbox.setSpacing(8); - hbox.setAlignment(Pos.CENTER); - hbox.getChildren().add(button); - hbox.getChildren().add(button2); - HBox.setHgrow(button, Priority.ALWAYS); - HBox.setHgrow(button2, Priority.ALWAYS); - } - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); + final ImageView iconView = new ImageView(); + final AutoTooltipButton button = new AutoTooltipButton(); + + { + button.setGraphic(iconView); + button.setGraphicTextGap(10); + button.setPrefWidth(10000); + } + + MaterialDesignIconView iconView2 = new MaterialDesignIconView(MaterialDesignIcon.PENCIL); + final AutoTooltipButton button2 = new AutoTooltipButton(); + + { + button2.setGraphic(iconView2); + button2.setGraphicTextGap(10); + button2.setPrefWidth(10000); + } + + final HBox hbox = new HBox(); + + { + hbox.setSpacing(8); + hbox.setAlignment(Pos.CENTER); + hbox.getChildren().add(button); + hbox.getChildren().add(button2); + HBox.setHgrow(button, Priority.ALWAYS); + HBox.setHgrow(button2, Priority.ALWAYS); + } + TableRow tableRow = getTableRow(); if (item != null && !empty) { Offer offer = item.getOffer(); @@ -1131,14 +1141,17 @@ abstract public class OfferBookView onRemoveOpenOffer(offer)); - iconView2.setId("image-edit"); + iconView2.setSize("16px"); button2.updateText(Res.get("shared.edit")); button2.setOnAction(e -> onEditOpenOffer(offer)); button2.setManaged(true); button2.setVisible(true); } else { boolean isSellOffer = OfferViewUtil.isShownAsSellOffer(offer); - iconView.setId(isSellOffer ? "image-buy-white" : "image-sell-white"); + boolean isPrivateOffer = offer.isPrivateOffer(); + iconView.setId(isPrivateOffer ? "image-lock2x" : isSellOffer ? "image-buy-white" : "image-sell-white"); + iconView.setFitHeight(16); + iconView.setFitWidth(16); button.setId(isSellOffer ? "buy-button" : "sell-button"); button.setStyle("-fx-text-fill: white"); title = Res.get("offerbook.takeOffer"); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index f5fdd74509..9a52902f3e 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -130,6 +130,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; boolean useOffersMatchingMyAccountsFilter; + boolean showPrivateOffers; /////////////////////////////////////////////////////////////////////////////////////////// @@ -172,6 +173,13 @@ abstract class OfferBookViewModel extends ActivatableViewModel { tradeCurrencyListChangeListener = c -> fillCurrencies(); + // refresh filter on changes + // 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() .max(Comparator.comparingLong(o -> o.getOffer().getAmount().longValueExact())); @@ -213,6 +221,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); } useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); + showPrivateOffers = preferences.isShowPrivateOffers(); fillCurrencies(); updateSelectedTradeCurrency(); @@ -258,7 +267,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel { showAllTradeCurrenciesProperty.set(showAllEntry); if (isEditEntry(code)) navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class); - else if (!showAllEntry) { + else if (showAllEntry) { + this.selectedTradeCurrency = getDefaultTradeCurrency(); + tradeCurrencyCode.set(selectedTradeCurrency.getCode()); + } else { this.selectedTradeCurrency = tradeCurrency; tradeCurrencyCode.set(code); } @@ -285,7 +297,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { if (!showAllPaymentMethods) { this.selectedPaymentMethod = paymentMethod; - // If we select TransferWise we switch to show all currencies as TransferWise supports + // If we select Wise we switch to show all currencies as Wise supports // sending to most currencies. if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { onSetTradeCurrency(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); @@ -307,6 +319,12 @@ abstract class OfferBookViewModel extends ActivatableViewModel { filterOffers(); } + void onShowPrivateOffers(boolean isSelected) { + showPrivateOffers = isSelected; + preferences.setShowPrivateOffers(isSelected); + filterOffers(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -402,9 +420,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } public Optional getMarketBasedPrice(Offer offer) { - OfferDirection displayDirection = offer.isTraditionalOffer() ? direction : - direction.equals(OfferDirection.BUY) ? OfferDirection.SELL : OfferDirection.BUY; - return priceUtil.getMarketBasedPrice(offer, displayDirection); + return priceUtil.getMarketBasedPrice(offer, direction); } String formatMarketPriceMarginPct(Offer offer) { @@ -474,8 +490,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { if (countryCode != null) { result += "\n" + Res.get("payment.f2f.offerbook.tooltip.countryAndCity", CountryUtil.getNameByCode(countryCode), offer.getF2FCity()); - - result += "\n" + Res.get("payment.f2f.offerbook.tooltip.extra", offer.getExtraInfo()); } } else { if (countryCode != null) { @@ -505,6 +519,8 @@ abstract class OfferBookViewModel extends ActivatableViewModel { result += "\n" + Res.getWithCol("shared.acceptedBanks") + " " + Joiner.on(", ").join(acceptedBanks); } } + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) + result += "\n" + Res.get("payment.shared.extraInfo.tooltip", offer.getCombinedExtraInfo()); } return result; } @@ -573,6 +589,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel { getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); + // filter private offers + if (direction == OfferDirection.BUY) { + predicate = predicate.and(offerBookListItem -> offerBookListItem.getOffer().isPrivateOffer() == showPrivateOffers); + } + if (!filterText.isEmpty()) { // filter node address @@ -583,10 +604,29 @@ abstract class OfferBookViewModel extends ActivatableViewModel { nextPredicate = nextPredicate.or(offerBookListItem -> offerBookListItem.getOffer().getId().toLowerCase().contains(filterText.toLowerCase())); - // filter payment method + // filter full payment method nextPredicate = nextPredicate.or(offerBookListItem -> Res.get(offerBookListItem.getOffer().getPaymentMethod().getId()).toLowerCase().contains(filterText.toLowerCase())); + // filter short payment method + nextPredicate = nextPredicate.or(offerBookListItem -> { + return getPaymentMethod(offerBookListItem).toLowerCase().contains(filterText.toLowerCase()); + }); + + // filter currencies + nextPredicate = nextPredicate.or(offerBookListItem -> { + return offerBookListItem.getOffer().getCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || + offerBookListItem.getOffer().getBaseCurrencyCode().toLowerCase().contains(filterText.toLowerCase()) || + CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()) || + CurrencyUtil.getNameAndCode(offerBookListItem.getOffer().getBaseCurrencyCode()).toLowerCase().contains(filterText.toLowerCase()); + }); + + // filter extra info + nextPredicate = nextPredicate.or(offerBookListItem -> { + return offerBookListItem.getOffer().getCombinedExtraInfo() != null && + offerBookListItem.getOffer().getCombinedExtraInfo().toLowerCase().contains(filterText.toLowerCase()); + }); + filteredItems.setPredicate(predicate.and(nextPredicate)); } else { filteredItems.setPredicate(predicate); @@ -629,9 +669,10 @@ abstract class OfferBookViewModel extends ActivatableViewModel { public boolean hasSelectionAccountSigning() { if (showAllTradeCurrenciesProperty.get()) { - if (!isShowAllEntry(selectedPaymentMethod.getId())) { + if (isShowAllEntry(selectedPaymentMethod.getId())) + return !(this instanceof CryptoOfferBookViewModel); + else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod); - } } else { if (isShowAllEntry(selectedPaymentMethod.getId())) return CurrencyUtil.getMatureMarketCurrencies().stream() @@ -639,7 +680,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { else return PaymentMethod.hasChargebackRisk(selectedPaymentMethod, tradeCurrencyCode.get()); } - return true; } private static String getDirectionWithCodeDetailed(OfferDirection direction, String currencyCode) { @@ -697,6 +737,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { abstract String getCurrencyCodeFromPreferences(OfferDirection direction); public OpenOffer getOpenOffer(Offer offer) { - return openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + return openOfferManager.getOpenOffer(offer.getId()).orElse(null); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.java index 71d18d50ed..14d1f83564 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OtherOfferBookView.java @@ -37,25 +37,25 @@ public class OtherOfferBookView extends OfferBookView filterPaymentMethods(ObservableList list, TradeCurrency selectedTradeCurrency) { return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> { - if (paymentMethod.isBlockchain()) return true; if (paymentMethod.getSupportedAssetCodes() == null) return true; for (String assetCode : paymentMethod.getSupportedAssetCodes()) { if (!CurrencyUtil.isFiatCurrency(assetCode)) return true; @@ -95,20 +93,13 @@ public class OtherOfferBookViewModel extends OfferBookViewModel { @Override void fillCurrencies(ObservableList tradeCurrencies, ObservableList allCurrencies) { - tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); - tradeCurrencies.addAll(preferences.getCryptoCurrenciesAsObservable().stream() - .filter(withoutTopCrypto()) - .collect(Collectors.toList())); tradeCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withoutFiatCurrency()) .collect(Collectors.toList())); tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); - allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream() - .filter(withoutTopCrypto()) - .collect(Collectors.toList())); allCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream() .filter(withoutFiatCurrency()) .collect(Collectors.toList())); @@ -120,12 +111,9 @@ public class OtherOfferBookViewModel extends OfferBookViewModel { TradeCurrency selectedTradeCurrency) { return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); - // BUY Crypto is actually SELL Monero - boolean directionResult = offer.getDirection() == direction; - boolean currencyResult = !CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - ((showAllTradeCurrenciesProperty.get() && - !offer.getCurrencyCode().equals(GUIUtil.TOP_CRYPTO.getCode())) || - offer.getCurrencyCode().equals(selectedTradeCurrency.getCode())); + boolean directionResult = offer.getDirection() != direction; + boolean currencyResult = CurrencyUtil.isTraditionalCurrency(offer.getCurrencyCode()) && !CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + (showAllTradeCurrenciesProperty.get() || offer.getCurrencyCode().equals(selectedTradeCurrency.getCode())); boolean paymentMethodResult = showAllPaymentMethods || offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); @@ -137,8 +125,8 @@ public class OtherOfferBookViewModel extends OfferBookViewModel { TradeCurrency getDefaultTradeCurrency() { TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency(); - if (!CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) && - !defaultTradeCurrency.equals(GUIUtil.TOP_CRYPTO) && + if (CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) && + !CurrencyUtil.isFiatCurrency(defaultTradeCurrency.getCode()) && hasPaymentAccountForCurrency(defaultTradeCurrency)) { return defaultTradeCurrency; } @@ -152,22 +140,19 @@ public class OtherOfferBookViewModel extends OfferBookViewModel { !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()); return sortedList.get(0); } else { - return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) -> - Boolean.compare(!hasPaymentAccountForCurrency(o1), - !hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0); + return CurrencyUtil.getMainTraditionalCurrencies().stream() + .filter(withoutFiatCurrency()) + .sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2))) + .collect(Collectors.toList()).get(0); } } @Override String getCurrencyCodeFromPreferences(OfferDirection direction) { - return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() : - preferences.getSellScreenCryptoCurrencyCode(); - } + // validate if previous stored currencies are Traditional ones + String currencyCode = direction == OfferDirection.BUY ? preferences.getBuyScreenOtherCurrencyCode() : preferences.getSellScreenOtherCurrencyCode(); - @NotNull - private Predicate withoutTopCrypto() { - return cryptoCurrency -> - !cryptoCurrency.equals(GUIUtil.TOP_CRYPTO); + return CurrencyUtil.isTraditionalCurrency(currencyCode) ? currencyCode : null; } @NotNull diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookViewModel.java deleted file mode 100644 index 37cdaa129a..0000000000 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/TopCryptoOfferBookViewModel.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package haveno.desktop.main.offer.offerbook; - -import com.google.inject.Inject; -import com.google.inject.name.Named; -import haveno.core.account.witness.AccountAgeWitnessService; -import haveno.core.api.CoreApi; -import haveno.core.locale.TradeCurrency; -import haveno.core.offer.Offer; -import haveno.core.offer.OfferDirection; -import haveno.core.offer.OfferFilterService; -import haveno.core.offer.OpenOfferManager; -import haveno.core.payment.payload.PaymentMethod; -import haveno.core.provider.price.PriceFeedService; -import haveno.core.trade.ClosedTradableManager; -import haveno.core.user.Preferences; -import haveno.core.user.User; -import haveno.core.util.FormattingUtils; -import haveno.core.util.PriceUtil; -import haveno.core.util.coin.CoinFormatter; -import haveno.core.xmr.setup.WalletsSetup; -import haveno.desktop.Navigation; -import haveno.desktop.util.GUIUtil; -import haveno.network.p2p.P2PService; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -public class TopCryptoOfferBookViewModel extends OfferBookViewModel { - - public static TradeCurrency TOP_CRYPTO = GUIUtil.TOP_CRYPTO; - - @Inject - public TopCryptoOfferBookViewModel(User user, - OpenOfferManager openOfferManager, - OfferBook offerBook, - Preferences preferences, - WalletsSetup walletsSetup, - P2PService p2PService, - PriceFeedService priceFeedService, - ClosedTradableManager closedTradableManager, - AccountAgeWitnessService accountAgeWitnessService, - Navigation navigation, - PriceUtil priceUtil, - OfferFilterService offerFilterService, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - CoreApi coreApi) { - super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi); - } - - @Override - protected void activate() { - super.activate(); - TOP_CRYPTO = GUIUtil.TOP_CRYPTO; - } - - @Override - void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) { - // No need to store anything as it is just one Crypto offers anyway - } - - @Override - protected ObservableList filterPaymentMethods(ObservableList list, - TradeCurrency selectedTradeCurrency) { - return FXCollections.observableArrayList(list.stream().filter(PaymentMethod::isBlockchain).collect(Collectors.toList())); - } - - @Override - void fillCurrencies(ObservableList tradeCurrencies, - ObservableList allCurrencies) { - tradeCurrencies.add(TOP_CRYPTO); - allCurrencies.add(TOP_CRYPTO); - } - - @Override - Predicate getCurrencyAndMethodPredicate(OfferDirection direction, - TradeCurrency selectedTradeCurrency) { - return offerBookListItem -> { - Offer offer = offerBookListItem.getOffer(); - // BUY Crypto is actually SELL Bitcoin - boolean directionResult = offer.getDirection() == direction; - boolean currencyResult = offer.getCurrencyCode().equals(TOP_CRYPTO.getCode()); - boolean paymentMethodResult = showAllPaymentMethods || - offer.getPaymentMethod().equals(selectedPaymentMethod); - boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); - return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; - }; - } - - @Override - TradeCurrency getDefaultTradeCurrency() { - return TOP_CRYPTO; - } - - @Override - String getCurrencyCodeFromPreferences(OfferDirection direction) { - return TOP_CRYPTO.getCode(); - } -} diff --git a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java index 311cca3476..7c1cd7f539 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/signedoffer/SignedOffersDataModel.java @@ -62,7 +62,9 @@ class SignedOffersDataModel extends ActivatableDataModel { private void applyList() { list.clear(); - list.addAll(openOfferManager.getObservableSignedOffersList().stream().map(SignedOfferListItem::new).collect(Collectors.toList())); + synchronized (openOfferManager.getObservableSignedOffersList()) { + list.addAll(openOfferManager.getObservableSignedOffersList().stream().map(SignedOfferListItem::new).collect(Collectors.toList())); + } // we sort by date, the earliest first list.sort((o1, o2) -> new Date(o2.getSignedOffer().getTimeStamp()).compareTo(new Date(o1.getSignedOffer().getTimeStamp()))); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 84210538a9..1a548f1b1f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -20,6 +20,7 @@ package haveno.desktop.main.offer.takeoffer; import com.google.inject.Inject; import haveno.common.ThreadUtils; +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; @@ -131,7 +132,7 @@ class TakeOfferDataModel extends OfferDataModel { addListeners(); - updateAvailableBalance(); + updateBalances(); // TODO In case that we have funded but restarted, or canceled but took again the offer we would need to // store locally the result when we received the funding tx(s). @@ -182,7 +183,7 @@ class TakeOfferDataModel extends OfferDataModel { checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()"); paymentAccount = getLastSelectedPaymentAccount(); - this.amount.set(offer.getAmount().min(BigInteger.valueOf(getMaxTradeLimit()))); + this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); updateSecurityDeposit(); @@ -192,7 +193,7 @@ class TakeOfferDataModel extends OfferDataModel { balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { @Override public void onBalanceChanged(BigInteger balance) { - updateAvailableBalance(); + updateBalances(); } }; @@ -227,6 +228,19 @@ class TakeOfferDataModel extends OfferDataModel { ThreadUtils.submitToPool(() -> xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId())); } + protected void updateBalances() { + super.updateBalances(); + + // update remaining balance + UserThread.await(() -> { + missingCoin.set(offerUtil.getBalanceShortage(totalToPay.get(), balance.get())); + isXmrWalletFunded.set(offerUtil.isBalanceSufficient(totalToPay.get(), availableBalance.get())); + if (totalToPay.get() != null && isXmrWalletFunded.get() && !showWalletFundedNotification.get()) { + showWalletFundedNotification.set(true); + } + }); + } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions @@ -270,6 +284,7 @@ class TakeOfferDataModel extends OfferDataModel { // handle error if (errorMsg != null) { new Popup().warning(errorMsg).show(); + log.warn("Error taking offer " + offer.getId() + ": " + errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); } } @@ -278,8 +293,7 @@ class TakeOfferDataModel extends OfferDataModel { if (paymentAccount != null) { this.paymentAccount = paymentAccount; - long myLimit = getMaxTradeLimit(); - this.amount.set(offer.getMinAmount().max(amount.get().min(BigInteger.valueOf(myLimit)))); + this.amount.set(BigInteger.valueOf(getMaxTradeLimit())); preferences.setTakeOfferSelectedPaymentAccountId(paymentAccount.getId()); } @@ -287,11 +301,7 @@ class TakeOfferDataModel extends OfferDataModel { void fundFromSavingsWallet() { useSavingsWallet = true; - updateAvailableBalance(); - if (!isXmrWalletFunded.get()) { - this.useSavingsWallet = false; - updateAvailableBalance(); - } + updateBalances(); } @@ -328,15 +338,19 @@ class TakeOfferDataModel extends OfferDataModel { .orElse(firstItem); } - long getMaxTradeLimit() { + long getMyMaxTradeLimit() { if (paymentAccount != null) { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), - offer.getMirroredDirection()); + offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()); } else { return 0; } } + long getMaxTradeLimit() { + return Math.min(offer.getAmount().longValueExact(), getMyMaxTradeLimit()); + } + boolean canTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && GUIUtil.isBootstrappedOrShowPopup(p2PService); @@ -369,12 +383,14 @@ class TakeOfferDataModel extends OfferDataModel { volume.set(volumeByAmount); - updateAvailableBalance(); + updateBalances(); } } - void applyAmount(BigInteger amount) { - this.amount.set(amount.min(BigInteger.valueOf(getMaxTradeLimit()))); + void maybeApplyAmount(BigInteger amount) { + if (amount.compareTo(offer.getMinAmount()) >= 0 && amount.compareTo(BigInteger.valueOf(getMaxTradeLimit())) <= 0) { + this.amount.set(amount); + } calculateTotalToPay(); } @@ -391,7 +407,7 @@ class TakeOfferDataModel extends OfferDataModel { totalToPay.set(feeAndSecDeposit.add(amount.get())); else totalToPay.set(feeAndSecDeposit); - updateAvailableBalance(); + updateBalances(); log.debug("totalToPay {}", totalToPay.get()); } } 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 1b0867ef7d..58f6bf4158 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 @@ -69,6 +69,7 @@ import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; import static haveno.desktop.util.FormBuilder.addAddressTextField; import static haveno.desktop.util.FormBuilder.addBalanceTextField; import static haveno.desktop.util.FormBuilder.addComboBoxTopLabelTextField; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addFundsTextfield; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.getEditableValueBox; @@ -98,6 +99,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; @@ -123,6 +125,9 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountsComboBox; + private TextArea extraInfoTextArea; private Label amountDescriptionLabel, paymentMethodLabel, priceCurrencyLabel, priceAsPercentageLabel, @@ -154,12 +160,13 @@ public class TakeOfferView extends ActivatableViewAndModel missingCoinListener; private int gridRow = 0; private final HashMap paymentAccountWarningDisplayed = new HashMap<>(); - private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, + private boolean offerDetailsWindowDisplayed, extraInfoPopupDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, - australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed; + australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed, F2FWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, getShowWalletFundedNotificationListener; @@ -191,6 +198,8 @@ public class TakeOfferView extends ActivatableViewAndModel close(false)) .show(); + + if (offer.hasBuyerAsTakerWithoutDeposit() && offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + + // attach extra info text area + extraInfoTextArea = addCompactTopLabelTextArea(gridPane, ++lastGridRowNoFundingRequired, Res.get("payment.shared.extraInfo.noDeposit"), "").second; + extraInfoTextArea.setText(offer.getCombinedExtraInfo()); + extraInfoTextArea.getStyleClass().add("text-area"); + extraInfoTextArea.setWrapText(true); + extraInfoTextArea.setPrefHeight(75); + extraInfoTextArea.setMinHeight(75); + extraInfoTextArea.setMaxHeight(150); + extraInfoTextArea.setEditable(false); + GridPane.setRowIndex(extraInfoTextArea, lastGridRowNoFundingRequired); + GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); + GridPane.setColumnIndex(extraInfoTextArea, 0); + + // move up take offer buttons + GridPane.setRowIndex(takeOfferBox, lastGridRowNoFundingRequired + 1); + GridPane.setMargin(takeOfferBox, new Insets(15, 0, 0, 0)); + } } @Override @@ -449,7 +474,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(); @@ -675,7 +709,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + if (!newValue.toString().equals("")) { + updateQrCode(); + } + }; + } + private void addListeners() { amountTextField.focusedProperty().addListener(amountFocusedListener); model.dataModel.getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); + model.dataModel.getMissingCoin().addListener(missingCoinListener); } private void removeListeners() { amountTextField.focusedProperty().removeListener(amountFocusedListener); model.dataModel.getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); + model.dataModel.getMissingCoin().removeListener(missingCoinListener); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -830,7 +874,25 @@ public class TakeOfferView extends ActivatableViewAndModel { String key = "CreateOfferCancelAndFunded"; - if (model.dataModel.getIsXmrWalletFunded().get() && + if (model.dataModel.getIsXmrWalletFunded().get() && model.dataModel.hasTotalToPay() && model.dataModel.preferences.showAgain(key)) { new Popup().backgroundInfo(Res.get("takeOffer.alreadyFunded.askCancel")) .closeButtonText(Res.get("shared.no")) @@ -1079,6 +1141,21 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getCombinedExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } private void maybeShowTakeOfferFromUnsignedAccountWarning(Offer offer) { // warn if you are selling BTC to unsigned account (#5343) @@ -1109,91 +1186,6 @@ public class TakeOfferView extends ActivatableViewAndModel { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowCashAtAtmWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CASH_AT_ATM_ID) && - !cashAtAtmWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - cashAtAtmWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowAustraliaPayidWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.AUSTRALIA_PAYID_ID) && - !australiaPayidWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - australiaPayidWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowPayPalWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.PAYPAL_ID) && - !paypalWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - paypalWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - - private void maybeShowCashAppWarning(PaymentAccount paymentAccount, Offer offer) { - if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CASH_APP_ID) && - !cashAppWarningDisplayed && !offer.getExtraInfo().isEmpty()) { - cashAppWarningDisplayed = true; - UserThread.runAfter(() -> { - new GenericMessageWindow() - .preamble(Res.get("payment.tradingRestrictions")) - .instruction(offer.getExtraInfo()) - .actionButtonText(Res.get("shared.iConfirm")) - .closeButtonText(Res.get("shared.close")) - .width(Layout.INITIAL_WINDOW_WIDTH) - .onClose(() -> close(false)) - .show(); - }, 500, TimeUnit.MILLISECONDS); - } - } - private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); @@ -1232,11 +1224,11 @@ public class TakeOfferView extends ActivatableViewAndModel im errorMessage.set(offer.getErrorMessage()); xmrValidator.setMaxValue(offer.getAmount()); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit()).min(offer.getAmount())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); xmrValidator.setMinValue(offer.getMinAmount()); } @@ -237,7 +237,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public void onPaymentAccountSelected(PaymentAccount paymentAccount) { dataModel.onPaymentAccountSelected(paymentAccount); - xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit()).min(offer.getAmount())); + xmrValidator.setMaxTradeLimit(BigInteger.valueOf(dataModel.getMaxTradeLimit())); updateButtonDisableState(); } @@ -299,20 +299,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel im Price tradePrice = dataModel.tradePrice; long maxTradeLimit = dataModel.getMaxTradeLimit(); if (PaymentMethod.isRoundedForAtmCash(dataModel.getPaymentMethod().getId())) { - BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), - tradePrice, - maxTradeLimit); - dataModel.applyAmount(adjustedAmountForAtm); - amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); + BigInteger adjustedAmountForAtm = CoinUtil.getRoundedAtmCashAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit); + dataModel.maybeApplyAmount(adjustedAmountForAtm); } else if (dataModel.getOffer().isTraditionalOffer()) { - if (!isAmountEqualMinAmount(dataModel.getAmount().get()) && (!isAmountEqualMaxAmount(dataModel.getAmount().get()))) { - // 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 - BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); - dataModel.applyAmount(roundedAmount); - } - amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); + BigInteger roundedAmount = CoinUtil.getRoundedAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); + dataModel.maybeApplyAmount(roundedAmount); } + amount.set(HavenoUtils.formatXmr(dataModel.getAmount().get())); if (!dataModel.isMinAmountLessOrEqualAmount()) amountValidationResult.set(new InputValidator.ValidationResult(false, @@ -580,25 +573,14 @@ class TakeOfferViewModel extends ActivatableWithDataModel im if (price != null) { if (dataModel.isRoundedForAtmCash()) { amount = CoinUtil.getRoundedAtmCashAmount(amount, price, maxTradeLimit); - } else if (dataModel.getOffer().isTraditionalOffer() - && !isAmountEqualMinAmount(amount) && !isAmountEqualMaxAmount(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 + } else if (dataModel.getOffer().isTraditionalOffer()) { amount = CoinUtil.getRoundedAmount(amount, price, maxTradeLimit, dataModel.getOffer().getCurrencyCode(), dataModel.getOffer().getPaymentMethodId()); } } - dataModel.applyAmount(amount); + dataModel.maybeApplyAmount(amount); } } - private boolean isAmountEqualMinAmount(BigInteger amount) { - return offer.getMinAmount().equals(amount); - } - - private boolean isAmountEqualMaxAmount(BigInteger amount) { - return offer.getAmount().equals(amount); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// 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 f937fb4add..e216b14ed9 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -394,7 +394,7 @@ public abstract class Overlay> { public T useReportBugButton() { this.closeButtonText = Res.get("shared.reportBug"); - this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://haveno.exchange/source/haveno/issues")); + this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues")); return cast(); } @@ -765,9 +765,7 @@ public abstract class Overlay> { FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em"); copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { - String forClipboard = headLineLabel.getText() + System.lineSeparator() + message - + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); - Utilities.copyToClipboard(forClipboard); + Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) mouseEvent.getSource(); UserThread.runAfter(() -> tp.hide(), 1); @@ -939,7 +937,7 @@ public abstract class Overlay> { gitHubButton.setOnAction(event -> { if (message != null) Utilities.copyToClipboard(message); - GUIUtil.openWebPage("https://haveno.exchange/source/haveno/issues"); + GUIUtil.openWebPage("https://github.com/haveno-dex/haveno/issues"); hide(); }); } @@ -1083,6 +1081,11 @@ public abstract class Overlay> { return isDisplayed; } + public String getClipboardText() { + return headLineLabel.getText() + System.lineSeparator() + message + + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); + } + @Override public String toString() { return "Popup{" + 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 new file mode 100644 index 0000000000..bd169b3a4b --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/overlays/editor/PasswordPopup.java @@ -0,0 +1,245 @@ +/* + * 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.desktop.main.overlays.editor; + +import haveno.common.util.Utilities; +import haveno.core.locale.GlobalSettings; +import haveno.desktop.components.InputTextField; +import haveno.desktop.main.overlays.Overlay; +import haveno.desktop.util.FormBuilder; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.value.ChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.scene.Camera; +import javafx.scene.PerspectiveCamera; +import javafx.scene.Scene; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.transform.Rotate; +import javafx.stage.Modality; +import javafx.util.Duration; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Consumer; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import static haveno.desktop.util.FormBuilder.addInputTextField; + +@Slf4j +public class PasswordPopup extends Overlay { + private InputTextField inputTextField; + private static PasswordPopup INSTANCE; + private Consumer actionHandler; + private ChangeListener focusListener; + private EventHandler keyEventEventHandler; + + public PasswordPopup() { + width = 600; + type = Type.Confirmation; + if (INSTANCE != null) + INSTANCE.hide(); + INSTANCE = this; + } + + public PasswordPopup onAction(Consumer confirmHandler) { + this.actionHandler = confirmHandler; + return this; + } + + @Override + public void show() { + actionButtonText("CONFIRM"); + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + onShow(); + } + + @Override + protected void onShow() { + super.display(); + + if (stage != null) { + focusListener = (observable, oldValue, newValue) -> { + if (!newValue) + hide(); + }; + stage.focusedProperty().addListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + @Override + public void hide() { + animateHide(); + } + + @Override + protected void onHidden() { + INSTANCE = null; + + if (stage != null) { + if (focusListener != null) + stage.focusedProperty().removeListener(focusListener); + + Scene scene = stage.getScene(); + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + } + + private void addContent() { + gridPane.setPadding(new Insets(64)); + + inputTextField = addInputTextField(gridPane, ++rowIndex, null, -10d); + GridPane.setColumnSpan(inputTextField, 2); + inputTextField.requestFocus(); + + keyEventEventHandler = event -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { + doClose(); + } + }; + } + + @Override + protected void addHeadLine() { + super.addHeadLine(); + GridPane.setHalignment(headLineLabel, HPos.CENTER); + } + + protected void setupKeyHandler(Scene scene) { + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + e.consume(); + doClose(); + } + if (e.getCode() == KeyCode.ENTER) { + e.consume(); + apply(); + } + }); + } + + @Override + protected void animateHide(Runnable onFinishedHandler) { + if (GlobalSettings.getUseAnimations()) { + double duration = getDuration(300); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + + gridPane.setRotationAxis(Rotate.X_AXIS); + Camera camera = gridPane.getScene().getCamera(); + gridPane.getScene().setCamera(new PerspectiveCamera()); + + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.rotateProperty(), 0, interpolator), + new KeyValue(gridPane.opacityProperty(), 1, interpolator) + )); + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.rotateProperty(), -90, interpolator), + new KeyValue(gridPane.opacityProperty(), 0, interpolator) + )); + timeline.setOnFinished(event -> { + gridPane.setRotate(0); + gridPane.setRotationAxis(Rotate.Z_AXIS); + gridPane.getScene().setCamera(camera); + onFinishedHandler.run(); + }); + timeline.play(); + } else { + onFinishedHandler.run(); + } + } + + @Override + protected void animateDisplay() { + if (GlobalSettings.getUseAnimations()) { + double startY = -160; + double duration = getDuration(400); + Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); + Timeline timeline = new Timeline(); + ObservableList keyFrames = timeline.getKeyFrames(); + keyFrames.add(new KeyFrame(Duration.millis(0), + new KeyValue(gridPane.opacityProperty(), 0, interpolator), + new KeyValue(gridPane.translateYProperty(), startY, interpolator) + )); + + keyFrames.add(new KeyFrame(Duration.millis(duration), + new KeyValue(gridPane.opacityProperty(), 1, interpolator), + new KeyValue(gridPane.translateYProperty(), 0, interpolator) + )); + + timeline.play(); + } + } + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(15, 15, 30, 30)); + } + + @Override + protected void addButtons() { + buttonDistance = 10; + super.addButtons(); + + actionButton.setOnAction(event -> apply()); + } + + private void apply() { + hide(); + if (actionHandler != null && inputTextField != null) + actionHandler.accept(inputTextField.getText()); + } + + @Override + protected void applyStyles() { + super.applyStyles(); + FormBuilder.getIconForLabel(AwesomeIcon.LOCK, headlineIcon, "1.5em"); + } + + @Override + protected void setModality() { + stage.initOwner(owner.getScene().getWindow()); + stage.initModality(Modality.NONE); + } + + @Override + protected void addEffectToBackground() { + } + + @Override + protected void removeEffectFromBackground() { + } +} 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 262ec5efea..6a238e56ee 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 @@ -141,7 +141,7 @@ public class ContractWindow extends Overlay { DisplayUtils.formatDateTime(offer.getDate()) + " / " + DisplayUtils.formatDateTime(dispute.getTradeDate())); String currencyCode = offer.getCurrencyCode(); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.offerType"), - DisplayUtils.getDirectionBothSides(offer.getDirection())); + DisplayUtils.getDirectionBothSides(offer.getDirection(), offer.isPrivateOffer())); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradePrice"), FormattingUtils.formatPrice(contract.getPrice())); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), @@ -248,7 +248,8 @@ public class ContractWindow extends Overlay { } addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerDepositTransactionId"), contract.getMakerDepositTxHash()); - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); + if (contract.getTakerDepositTxHash() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.takerDepositTransactionId"), contract.getTakerDepositTxHash()); if (dispute.getDelayedPayoutTxId() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); 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 997a724224..ebf1c25309 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 @@ -99,6 +99,7 @@ public class DisputeSummaryWindow extends Overlay { reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, reasonWasPeerWasLateRadioButton, reasonWasTradeAlreadySettledRadioButton; + private CoreDisputesService.PayoutSuggestion payoutSuggestion; // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; @@ -700,33 +701,28 @@ public class DisputeSummaryWindow extends Overlay { } private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { - CoreDisputesService.DisputePayout payout; if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { - payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { - payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.BUYER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { - payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { - payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL; + payoutSuggestion = CoreDisputesService.PayoutSuggestion.SELLER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else { // should not happen throw new IllegalStateException("Unknown radio button"); } - disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1); + disputesService.applyPayoutAmountsToDisputeResult(payoutSuggestion, dispute, disputeResult, -1); buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost())); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost())); } private void applyTradeAmountRadioButtonStates() { - Contract contract = dispute.getContract(); - BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit(); - BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit(); - BigInteger tradeAmount = contract.getTradeAmount(); BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmountBeforeCost(); BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmountBeforeCost(); @@ -734,20 +730,22 @@ public class DisputeSummaryWindow extends Overlay { buyerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(buyerPayoutAmount)); sellerPayoutAmountInputTextField.setText(HavenoUtils.formatXmr(sellerPayoutAmount)); - if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit)) && - sellerPayoutAmount.equals(sellerSecurityDeposit)) { - buyerGetsTradeAmountRadioButton.setSelected(true); - } else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) && - sellerPayoutAmount.equals(BigInteger.ZERO)) { - buyerGetsAllRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(buyerSecurityDeposit)) { - sellerGetsTradeAmountRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(BigInteger.ZERO)) { - sellerGetsAllRadioButton.setSelected(true); - } else { - customRadioButton.setSelected(true); + switch (payoutSuggestion) { + case BUYER_GETS_TRADE_AMOUNT: + buyerGetsTradeAmountRadioButton.setSelected(true); + break; + case BUYER_GETS_ALL: + buyerGetsAllRadioButton.setSelected(true); + break; + case SELLER_GETS_TRADE_AMOUNT: + sellerGetsTradeAmountRadioButton.setSelected(true); + break; + case SELLER_GETS_ALL: + sellerGetsAllRadioButton.setSelected(true); + break; + case CUSTOM: + customRadioButton.setSelected(true); + break; } } } 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 e93dc647a3..dd645246f7 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 @@ -29,6 +29,7 @@ import haveno.core.locale.Res; import haveno.core.monetary.Price; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; @@ -42,6 +43,8 @@ import haveno.desktop.Navigation; import haveno.desktop.components.AutoTooltipButton; 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; @@ -174,18 +177,14 @@ public class OfferDetailsWindow extends Overlay { List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); - boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || - offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || - offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| - offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID)|| - offer.getPaymentMethod().equals(PaymentMethod.CASH_APP_ID); + boolean showOfferExtraInfo = offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty(); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) rows++; if (showAcceptedCountryCodes) rows++; - if (showExtraInfo) + if (showOfferExtraInfo) rows++; if (isF2F) rows++; @@ -195,7 +194,7 @@ public class OfferDetailsWindow extends Overlay { rows++; } - addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer")); + addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get(offer.isPrivateOffer() ? "shared.Offer" : "shared.Offer")); String counterCurrencyDirectionInfo = ""; String xmrDirectionInfo = ""; @@ -212,12 +211,12 @@ public class OfferDetailsWindow extends Overlay { xmrDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; } else if (placeOfferHandlerOptional.isPresent()) { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, - DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode), firstRowDistance); + DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode, offer.isPrivateOffer()), firstRowDistance); counterCurrencyDirectionInfo = direction == OfferDirection.SELL ? toReceive : toSpend; xmrDirectionInfo = direction == OfferDirection.BUY ? toReceive : toSpend; } else { addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel, - DisplayUtils.getDirectionBothSides(direction), firstRowDistance); + DisplayUtils.getDirectionBothSides(direction, offer.isPrivateOffer()), firstRowDistance); } String amount = Res.get("shared.xmrAmount"); if (takeOfferHandlerOptional.isPresent()) { @@ -316,9 +315,9 @@ public class OfferDetailsWindow extends Overlay { if (isF2F) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity()); } - if (showExtraInfo) { + if (showOfferExtraInfo) { TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second; - textArea.setText(offer.getExtraInfo()); + textArea.setText(offer.getCombinedExtraInfo()); textArea.setMaxHeight(200); textArea.sceneProperty().addListener((o, oldScene, newScene) -> { if (newScene != null) { @@ -342,6 +341,10 @@ public class OfferDetailsWindow extends Overlay { // get amount reserved for the offer BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; + // get offer challenge + OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOffer(offer.getId()).orElse(null); + String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge(); + rows = 3; if (countryCode != null) rows++; @@ -349,6 +352,8 @@ public class OfferDetailsWindow extends Overlay { rows++; if (reservedAmount != null) rows++; + if (offerChallenge != null) + rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE); addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(), @@ -359,12 +364,13 @@ public class OfferDetailsWindow extends Overlay { DisplayUtils.formatDateTime(offer.getDate())); String value = Res.getWithColAndCap("shared.buyer") + " " + - HavenoUtils.formatXmr(offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getBuyerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxBuyerSecurityDeposit(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + - HavenoUtils.formatXmr(offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); + HavenoUtils.formatXmr(takeOfferHandlerOptional.isPresent() ? offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(tradeAmount) : offer.getOfferPayload().getMaxSellerSecurityDeposit(), true); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value); + if (reservedAmount != null) { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.reservedAmount"), HavenoUtils.formatXmr(reservedAmount, true)); } @@ -373,6 +379,9 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); + if (offerChallenge != null) + addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.challenge"), offerChallenge); + if (placeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE); final Tuple2 labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"), @@ -416,13 +425,13 @@ public class OfferDetailsWindow extends Overlay { ++rowIndex, 1, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); - AutoTooltipButton button = (AutoTooltipButton) placeOfferTuple.first; - button.setMinHeight(40); - button.setPadding(new Insets(0, 20, 0, 20)); - button.setGraphic(iconView); - button.setGraphicTextGap(10); - button.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); - button.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); + 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); busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; @@ -436,29 +445,48 @@ public class OfferDetailsWindow extends Overlay { placeOfferTuple.fourth.getChildren().add(cancelButton); - button.setOnAction(e -> { + confirmButton.setOnAction(e -> { if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) { - button.setDisable(true); - cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent - // temporarily disabled due to high CPU usage (per issue #4649) - // busyAnimation.play(); - if (isPlaceOffer) { - spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); - placeOfferHandlerOptional.ifPresent(Runnable::run); + if (!isPlaceOffer && offer.isPrivateOffer()) { + new PasswordPopup() + .headLine(Res.get("offerbook.takeOffer.enterChallenge")) + .onAction(password -> { + if (offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(password))) { + offer.setChallenge(password); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); + } else { + new Popup().warning(Res.get("password.wrongPw")).show(); + } + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); } else { - - // subscribe to trade progress - spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); - numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { - subscribeToProgress(spinnerInfoLabel); - }); - - takeOfferHandlerOptional.ifPresent(Runnable::run); + confirmTakeOfferAux(confirmButton, cancelButton, spinnerInfoLabel, isPlaceOffer); } } }); } + private void confirmTakeOfferAux(Button button, Button cancelButton, Label spinnerInfoLabel, boolean isPlaceOffer) { + button.setDisable(true); + cancelButton.setDisable(isPlaceOffer ? false : true); // TODO: enable cancel button for taking an offer until messages sent + // temporarily disabled due to high CPU usage (per issue #4649) + // busyAnimation.play(); + if (isPlaceOffer) { + spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); + placeOfferHandlerOptional.ifPresent(Runnable::run); + } else { + + // subscribe to trade progress + spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo", "0%")); + numTradesSubscription = EasyBind.subscribe(tradeManager.getNumPendingTrades(), newNum -> { + subscribeToProgress(spinnerInfoLabel); + }); + + takeOfferHandlerOptional.ifPresent(Runnable::run); + } + } + private void subscribeToProgress(Label spinnerInfoLabel) { Trade trade = tradeManager.getTrade(offer.getId()); if (trade == null || initProgressSubscription != null) return; 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 223933e5c1..8bca62d143 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 @@ -37,10 +37,10 @@ import java.io.ByteArrayInputStream; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); private final ImageView qrCodeImageView; - private final String bitcoinURI; + private final String moneroUri; public QRCodeWindow(String bitcoinURI) { - this.bitcoinURI = bitcoinURI; + this.moneroUri = bitcoinURI; final byte[] imageBytes = QRCode .from(bitcoinURI) .withSize(300, 300) @@ -70,7 +70,7 @@ public class QRCodeWindow extends Overlay { GridPane.setHalignment(qrCodeImageView, HPos.CENTER); gridPane.getChildren().add(qrCodeImageView); - String request = bitcoinURI.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); + String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); infoLabel.setMouseTransparent(true); infoLabel.setWrapText(true); @@ -87,4 +87,8 @@ public class QRCodeWindow extends Overlay { applyStyles(); display(); } + + public String getClipboardText() { + return moneroUri; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java index 755e814076..18fe8fc4b4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/TacWindow.java @@ -20,11 +20,16 @@ package haveno.desktop.main.overlays.windows; import com.google.inject.Inject; import haveno.core.locale.Res; import haveno.desktop.app.HavenoApp; +import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.main.overlays.Overlay; +import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; +import javafx.scene.layout.GridPane; import javafx.stage.Screen; import lombok.extern.slf4j.Slf4j; +import static haveno.desktop.util.FormBuilder.addHyperlinkWithIcon; + @Slf4j public class TacWindow extends Overlay { @@ -91,11 +96,10 @@ public class TacWindow extends Overlay { String fontStyleClass = smallScreen ? "small-text" : "normal-text"; messageTextArea.getStyleClass().add(fontStyleClass); - // TODO: link to the wiki - // HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("tacWindow.arbitrationSystem"), - // "https://haveno.exchange/wiki/Dispute_resolution"); - // hyperlinkWithIcon.getStyleClass().add(fontStyleClass); - // GridPane.setMargin(hyperlinkWithIcon, new Insets(-6, 0, -20, -4)); + HyperlinkWithIcon hyperlinkWithIcon = addHyperlinkWithIcon(gridPane, ++rowIndex, Res.get("tacWindow.arbitrationSystem"), + "https://docs.haveno.exchange/the-project/dispute-resolution"); + hyperlinkWithIcon.getStyleClass().add(fontStyleClass); + GridPane.setMargin(hyperlinkWithIcon, new Insets(-6, 0, -20, -4)); } @Override 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 6575769db3..004cc0a3f5 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,6 +38,7 @@ 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 static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription; import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox; @@ -47,6 +48,8 @@ import static haveno.desktop.util.FormBuilder.addLabelTxIdTextField; 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; @@ -165,9 +168,12 @@ public class TradeDetailsWindow extends Overlay { // second group rows = 7; + + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) + rows++; + PaymentAccountPayload buyerPaymentAccountPayload = null; PaymentAccountPayload sellerPaymentAccountPayload = null; - if (contract != null) { rows++; @@ -183,12 +189,12 @@ public class TradeDetailsWindow extends Overlay { rows++; } - if (trade.getPayoutTxId() != null) - rows++; boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; + else if (trade.getPayoutTxId() != null) + rows++; if (trade.hasFailed()) rows += 2; if (trade.getTradePeerNodeAddress() != null) @@ -201,11 +207,11 @@ public class TradeDetailsWindow extends Overlay { DisplayUtils.formatDateTime(trade.getDate())); String securityDeposit = Res.getWithColAndCap("shared.buyer") + " " + - HavenoUtils.formatXmr(offer.getMaxBuyerSecurityDeposit(), true) + + HavenoUtils.formatXmr(trade.getBuyerSecurityDepositBeforeMiningFee(), true) + " / " + Res.getWithColAndCap("shared.seller") + " " + - HavenoUtils.formatXmr(offer.getMaxSellerSecurityDeposit(), true); + HavenoUtils.formatXmr(trade.getSellerSecurityDepositBeforeMiningFee(), true); addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); @@ -219,6 +225,29 @@ public class TradeDetailsWindow extends Overlay { addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"), trade.getTradePeerNodeAddress().getFullAddress()); + if (offer.getCombinedExtraInfo() != null && !offer.getCombinedExtraInfo().isEmpty()) { + 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.setEditable(false); + } + if (contract != null) { buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing()); sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing()); @@ -244,16 +273,18 @@ public class TradeDetailsWindow extends Overlay { if (trade.getMaker().getDepositTxHash() != null) 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 (trade.getPayoutTxId() != null && !trade.getPayoutTxId().isBlank()) - addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), - trade.getPayoutTxId()); - if (showDisputedTx) + + if (showDisputedTx) { addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); + } else if (trade.getPayoutTxId() != null && !trade.getPayoutTxId().isBlank()) { + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), + trade.getPayoutTxId()); + } if (trade.hasFailed()) { textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java index db5759a200..c3a1e76738 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/PortfolioView.java @@ -30,6 +30,8 @@ import haveno.desktop.common.view.CachingViewLoader; import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.View; import haveno.desktop.main.MainView; +import haveno.desktop.main.overlays.popups.Popup; +import haveno.desktop.main.portfolio.cloneoffer.CloneOfferView; import haveno.desktop.main.portfolio.closedtrades.ClosedTradesView; import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; import haveno.desktop.main.portfolio.editoffer.EditOfferView; @@ -49,7 +51,7 @@ public class PortfolioView extends ActivatableView { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab; - private Tab editOpenOfferTab, duplicateOfferTab; + private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab; private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase()); private Tab currentTab; private Navigation.Listener navigationListener; @@ -61,7 +63,8 @@ public class PortfolioView extends ActivatableView { private final FailedTradesManager failedTradesManager; private EditOfferView editOfferView; private DuplicateOfferView duplicateOfferView; - private boolean editOpenOfferViewOpen; + private CloneOfferView cloneOfferView; + private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen; private OpenOffer openOffer; private OpenOffersView openOffersView; @@ -99,12 +102,16 @@ public class PortfolioView extends ActivatableView { navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class); else if (newValue == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); + } else if (newValue == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); } if (oldValue != null && oldValue == editOpenOfferTab) editOfferView.onTabSelected(false); if (oldValue != null && oldValue == duplicateOfferTab) duplicateOfferView.onTabSelected(false); + if (oldValue != null && oldValue == cloneOpenOfferTab) + cloneOfferView.onTabSelected(false); }; @@ -115,6 +122,8 @@ public class PortfolioView extends ActivatableView { onEditOpenOfferRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab)) onDuplicateOfferRemoved(); + if (removedTabs.size() == 1 && removedTabs.get(0).equals(cloneOpenOfferTab)) + onCloneOpenOfferRemoved(); }; } @@ -137,11 +146,23 @@ public class PortfolioView extends ActivatableView { navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } + private void onCloneOpenOfferRemoved() { + cloneOpenOfferViewOpen = false; + if (cloneOfferView != null) { + cloneOfferView.onClose(); + cloneOfferView = null; + } + + navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); + } + @Override protected void activate() { failedTradesManager.getObservableList().addListener((ListChangeListener) c -> { - if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3) - root.getTabs().add(failedTradesTab); + UserThread.execute(() -> { + if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3) + root.getTabs().add(failedTradesTab); + }); }); if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3) root.getTabs().add(failedTradesTab); @@ -164,6 +185,9 @@ public class PortfolioView extends ActivatableView { } else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true); + } else if (root.getSelectionModel().getSelectedItem() == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); + if (cloneOfferView != null) cloneOfferView.onTabSelected(true); } } @@ -176,10 +200,9 @@ public class PortfolioView extends ActivatableView { } private void loadView(Class viewClass, @Nullable Object data) { - // we want to get activate/deactivate called, so we remove the old view on tab change - // TODO Don't understand the check for currentTab != editOpenOfferTab - if (currentTab != null && currentTab != editOpenOfferTab) - currentTab.setContent(null); + + // nullify current tab to trigger activate/deactivate + if (currentTab != null) currentTab.setContent(null); View view = viewLoader.load(viewClass); @@ -233,6 +256,28 @@ public class PortfolioView extends ActivatableView { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } + } else if (view instanceof CloneOfferView) { + if (data instanceof OpenOffer) { + openOffer = (OpenOffer) data; + } + if (openOffer != null) { + if (cloneOfferView == null) { + cloneOfferView = (CloneOfferView) view; + cloneOfferView.applyOpenOffer(openOffer); + cloneOpenOfferTab = new Tab(Res.get("portfolio.tab.cloneOpenOffer").toUpperCase()); + cloneOfferView.setCloseHandler(() -> { + root.getTabs().remove(cloneOpenOfferTab); + }); + root.getTabs().add(cloneOpenOfferTab); + } + if (currentTab != cloneOpenOfferTab) + cloneOfferView.onTabSelected(true); + + currentTab = cloneOpenOfferTab; + } else { + view = viewLoader.load(OpenOffersView.class); + selectOpenOffersView((OpenOffersView) view); + } } currentTab.setContent(view.getRoot()); @@ -243,20 +288,35 @@ public class PortfolioView extends ActivatableView { openOffersView = view; currentTab = openOffersTab; - OpenOfferActionHandler openOfferActionHandler = openOffer -> { + EditOpenOfferHandler editOpenOfferHandler = openOffer -> { if (!editOpenOfferViewOpen) { editOpenOfferViewOpen = true; PortfolioView.this.openOffer = openOffer; navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), EditOfferView.class); } else { - log.error("You have already a \"Edit Offer\" tab open."); + new Popup().warning(Res.get("editOffer.openTabWarning")).show(); } }; - openOffersView.setOpenOfferActionHandler(openOfferActionHandler); + openOffersView.setEditOpenOfferHandler(editOpenOfferHandler); + + CloneOpenOfferHandler cloneOpenOfferHandler = openOffer -> { + if (!cloneOpenOfferViewOpen) { + cloneOpenOfferViewOpen = true; + PortfolioView.this.openOffer = openOffer; + navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), CloneOfferView.class); + } else { + new Popup().warning(Res.get("cloneOffer.openTabWarning")).show(); + } + }; + openOffersView.setCloneOpenOfferHandler(cloneOpenOfferHandler); } - public interface OpenOfferActionHandler { + public interface EditOpenOfferHandler { void onEditOpenOffer(OpenOffer openOffer); } + + public interface CloneOpenOfferHandler { + void onCloneOpenOffer(OpenOffer openOffer); + } } 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 new file mode 100644 index 0000000000..24d792005c --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -0,0 +1,195 @@ +/* + * 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.desktop.main.portfolio.cloneoffer; + + +import haveno.desktop.Navigation; +import haveno.desktop.main.offer.MutableOfferDataModel; +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.TradeCurrency; +import haveno.core.offer.CreateOfferService; +import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; +import haveno.core.offer.OpenOfferManager; +import haveno.core.payment.PaymentAccount; +import haveno.core.proto.persistable.CorePersistenceProtoResolver; +import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.statistics.TradeStatisticsManager; +import haveno.core.user.Preferences; +import haveno.core.user.User; +import haveno.core.util.FormattingUtils; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.xmr.wallet.XmrWalletService; +import haveno.network.p2p.P2PService; + +import haveno.common.handlers.ErrorMessageHandler; +import haveno.common.handlers.ResultHandler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +class CloneOfferDataModel extends MutableOfferDataModel { + + private final CorePersistenceProtoResolver corePersistenceProtoResolver; + private OpenOffer sourceOpenOffer; + + @Inject + CloneOfferDataModel(CreateOfferService createOfferService, + OpenOfferManager openOfferManager, + OfferUtil offerUtil, + XmrWalletService xmrWalletService, + Preferences preferences, + User user, + P2PService p2PService, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, + CorePersistenceProtoResolver corePersistenceProtoResolver, + TradeStatisticsManager tradeStatisticsManager, + Navigation navigation) { + + super(createOfferService, + openOfferManager, + offerUtil, + xmrWalletService, + preferences, + user, + p2PService, + priceFeedService, + accountAgeWitnessService, + xmrFormatter, + tradeStatisticsManager, + navigation); + this.corePersistenceProtoResolver = corePersistenceProtoResolver; + } + + public void reset() { + direction = null; + tradeCurrency = null; + tradeCurrencyCode.set(null); + useMarketBasedPrice.set(false); + amount.set(null); + minAmount.set(null); + price.set(null); + volume.set(null); + minVolume.set(null); + securityDepositPct.set(0); + paymentAccounts.clear(); + paymentAccount = null; + marketPriceMarginPct = 0; + sourceOpenOffer = null; + } + + public void applyOpenOffer(OpenOffer openOffer) { + this.sourceOpenOffer = openOffer; + + Offer offer = openOffer.getOffer(); + direction = offer.getDirection(); + CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()) + .ifPresent(c -> this.tradeCurrency = c); + tradeCurrencyCode.set(offer.getCurrencyCode()); + + PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()); + if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { + TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); + this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); + if (paymentAccount.getSingleTradeCurrency() != null) + paymentAccount.setSingleTradeCurrency(selectedTradeCurrency); + else + paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); + } + + allowAmountUpdate = false; + } + + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { + try { + return super.initWithData(direction, tradeCurrency, false); + } catch (NullPointerException e) { + if (e.getMessage().contains("tradeCurrency")) { + throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); + } + return false; + } + } + + @Override + protected Set getUserPaymentAccounts() { + return Objects.requireNonNull(user.getPaymentAccounts()).stream() + .filter(account -> !account.getPaymentMethod().isBsqSwap()) + .collect(Collectors.toSet()); + } + + @Override + protected PaymentAccount getPreselectedPaymentAccount() { + return paymentAccount; + } + + public void populateData() { + Offer offer = sourceOpenOffer.getOffer(); + // Min amount need to be set before amount as if minAmount is null it would be set by amount + setMinAmount(offer.getMinAmount()); + setAmount(offer.getAmount()); + setPrice(offer.getPrice()); + setVolume(offer.getVolume()); + setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + setTriggerPrice(sourceOpenOffer.getTriggerPrice()); + if (offer.isUseMarketBasedPrice()) { + setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); + } + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); + setSecurityDepositPct(getSecurityAsPercent(offer)); + setExtraInfo(offer.getOfferExtraInfo()); + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Offer clonedOffer = createClonedOffer(); + openOfferManager.placeOffer(clonedOffer, + false, + triggerPrice, + false, + true, + sourceOpenOffer.getId(), + transaction -> resultHandler.handleResult(), + errorMessageHandler); + } + + private Offer createClonedOffer() { + return createOfferService.createClonedOffer(sourceOpenOffer.getOffer(), + tradeCurrencyCode.get(), + useMarketBasedPrice.get() ? null : price.get(), + useMarketBasedPrice.get(), + useMarketBasedPrice.get() ? marketPriceMarginPct : 0, + paymentAccount, + extraInfo.get()); + } + + public boolean hasConflictingClone() { + Offer clonedOffer = createClonedOffer(); + return openOfferManager.hasConflictingClone(clonedOffer, sourceOpenOffer); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml new file mode 100644 index 0000000000..80c57192c0 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..e48bdf80a7 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -0,0 +1,261 @@ +/* + * 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.desktop.main.portfolio.cloneoffer; + +import haveno.desktop.Navigation; +import haveno.desktop.common.view.FxmlView; +import haveno.desktop.components.AutoTooltipButton; +import haveno.desktop.components.BusyAnimation; +import haveno.desktop.main.offer.MutableOfferView; +import haveno.desktop.main.overlays.popups.Popup; +import haveno.desktop.main.overlays.windows.OfferDetailsWindow; + +import haveno.core.locale.CurrencyUtil; +import haveno.core.locale.Res; +import haveno.core.offer.OpenOffer; +import haveno.core.payment.PaymentAccount; +import haveno.core.user.DontShowAgainLookup; +import haveno.core.user.Preferences; +import haveno.core.util.FormattingUtils; +import haveno.core.util.coin.CoinFormatter; +import haveno.common.UserThread; +import haveno.common.util.Tuple4; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import javafx.collections.ObservableList; + +import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; + +@FxmlView +public class CloneOfferView extends MutableOfferView { + + private BusyAnimation busyAnimation; + private Button cloneButton; + private Button cancelButton; + private Label spinnerInfoLabel; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private CloneOfferView(CloneOfferViewModel model, + Navigation navigation, + Preferences preferences, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) { + super(model, navigation, preferences, offerDetailsWindow, btcFormatter); + } + + @Override + protected void initialize() { + super.initialize(); + + addCloneGroup(); + renameAmountGroup(); + } + + private void renameAmountGroup() { + amountTitledGroupBg.setText(Res.get("editOffer.setPrice")); + } + + @Override + protected void doSetFocus() { + // Don't focus in any field before data was set + } + + @Override + protected void doActivate() { + super.doActivate(); + + + addBindings(); + + hideOptionsGroup(); + hideNextButtons(); + + // Lock amount field as it would require bigger changes to support increased amount values. + amountTextField.setDisable(true); + amountBtcLabel.setDisable(true); + minAmountTextField.setDisable(true); + minAmountBtcLabel.setDisable(true); + volumeTextField.setDisable(true); + volumeCurrencyLabel.setDisable(true); + + // Workaround to fix margin on top of amount group + gridPane.setPadding(new Insets(-20, 25, -1, 25)); + + updatePriceToggle(); + updateElementsWithDirection(); + + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + + model.onInvalidateMarketPriceMargin(); + model.onInvalidatePrice(); + + // To force re-validation of payment account validation + onPaymentAccountsComboBoxSelected(); + } + + @Override + protected void deactivate() { + super.deactivate(); + + removeBindings(); + } + + @Override + public void onClose() { + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void applyOpenOffer(OpenOffer openOffer) { + model.applyOpenOffer(openOffer); + + initWithData(openOffer.getOffer().getDirection(), + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + false, + null); + + if (!model.isSecurityDepositValid()) { + new Popup().warning(Res.get("editOffer.invalidDeposit")) + .onClose(this::close) + .show(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings, Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBindings() { + cloneButton.disableProperty().bind(model.isNextButtonDisabled); + } + + private void removeBindings() { + cloneButton.disableProperty().unbind(); + } + + @Override + protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { + return paymentAccounts; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI elements + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addCloneGroup() { + Tuple4 tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 6, Res.get("cloneOffer.clone")); + + HBox hBox = tuple4.fourth; + hBox.setAlignment(Pos.CENTER_LEFT); + GridPane.setHalignment(hBox, HPos.LEFT); + + cloneButton = tuple4.first; + cloneButton.setMinHeight(40); + cloneButton.setPadding(new Insets(0, 20, 0, 20)); + cloneButton.setGraphicTextGap(10); + + busyAnimation = tuple4.second; + spinnerInfoLabel = tuple4.third; + + cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); + cancelButton.setDefaultButton(false); + cancelButton.setOnAction(event -> close()); + hBox.getChildren().add(cancelButton); + + cloneButton.setOnAction(e -> { + cloneButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) + onClone(); + }); + } + + private void onClone() { + if (model.dataModel.hasConflictingClone()) { + new Popup().warning(Res.get("cloneOffer.hasConflictingClone")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::doClone) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + doClone(); + } + } + + private void doClone() { + if (model.isPriceInRange()) { + model.isNextButtonDisabled.setValue(true); + cancelButton.setDisable(true); + busyAnimation.play(); + spinnerInfoLabel.setText(Res.get("cloneOffer.publishOffer")); + model.onCloneOffer(() -> { + UserThread.execute(() -> { + String key = "cloneOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("cloneOffer.success")) + .dontShowAgainId(key) + .show(); + } + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }); + }, + errorMessage -> { + UserThread.execute(() -> { + log.error(errorMessage); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(errorMessage).show(); + }); + }); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateElementsWithDirection() { + ImageView iconView = new ImageView(); + iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white"); + cloneButton.setGraphic(iconView); + cloneButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java new file mode 100644 index 0000000000..f36e18da79 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java @@ -0,0 +1,120 @@ +/* + * 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.desktop.main.portfolio.cloneoffer; + +import haveno.desktop.Navigation; +import haveno.desktop.main.offer.MutableOfferViewModel; +import haveno.desktop.main.offer.OfferViewUtil; + +import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; +import haveno.core.payment.validation.FiatVolumeValidator; +import haveno.core.payment.validation.SecurityDepositValidator; +import haveno.core.payment.validation.XmrValidator; +import haveno.core.provider.price.PriceFeedService; +import haveno.core.user.Preferences; +import haveno.core.util.FormattingUtils; +import haveno.core.util.PriceUtil; +import haveno.core.util.coin.CoinFormatter; +import haveno.core.util.validation.AmountValidator4Decimals; +import haveno.core.util.validation.AmountValidator8Decimals; +import haveno.common.handlers.ErrorMessageHandler; +import haveno.common.handlers.ResultHandler; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +class CloneOfferViewModel extends MutableOfferViewModel { + + @Inject + public CloneOfferViewModel(CloneOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + AmountValidator4Decimals priceValidator4Decimals, + AmountValidator8Decimals priceValidator8Decimals, + XmrValidator xmrValidator, + SecurityDepositValidator securityDepositValidator, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + Preferences preferences, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + OfferUtil offerUtil) { + super(dataModel, + fiatVolumeValidator, + priceValidator4Decimals, + priceValidator8Decimals, + xmrValidator, + securityDepositValidator, + priceFeedService, + accountAgeWitnessService, + navigation, + preferences, + btcFormatter, + offerUtil); + syncMinAmountWithAmount = false; + } + + @Override + public void activate() { + super.activate(); + + dataModel.populateData(); + + long triggerPriceAsLong = dataModel.getTriggerPrice(); + dataModel.setTriggerPrice(triggerPriceAsLong); + if (triggerPriceAsLong > 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + } + + public void applyOpenOffer(OpenOffer openOffer) { + dataModel.reset(); + dataModel.applyOpenOffer(openOffer); + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + dataModel.onCloneOffer(resultHandler, errorMessageHandler); + } + + public void onInvalidateMarketPriceMargin() { + marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct())); + } + + public void onInvalidatePrice() { + price.set(FormattingUtils.formatPrice(null)); + price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); + } + + public boolean isSecurityDepositValid() { + return securityDepositValidator.validate(securityDeposit.get()).isValid; + } + + @Override + public void triggerFocusOutOnAmountFields() { + // do not update BTC Amount or minAmount here + // issue 2798: "after a few edits of offer the BTC amount has increased" + } + + public boolean isShownAsSellOffer() { + return OfferViewUtil.isShownAsSellOffer(getTradeCurrency(), dataModel.getDirection()); + } +} \ No newline at end of file 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 6c1e30b828..7b6ca92a19 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 @@ -105,7 +105,7 @@ public class ClosedTradesListItem implements FilterableListItem { ? offer.getDirection() : offer.getMirroredDirection(); String currencyCode = tradable.getOffer().getCurrencyCode(); - return DisplayUtils.getDirectionWithCode(direction, currencyCode); + return DisplayUtils.getDirectionWithCode(direction, currencyCode, offer.isPrivateOffer()); } public Date getDate() { 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 3257d21059..ab92f845db 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 @@ -444,7 +444,7 @@ public class ClosedTradesView extends ActivatableViewAndModel onDuplicateOffer(item.getTradable().getOffer())); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 75514e4300..4db019c021 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -26,6 +26,7 @@ import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; @@ -34,13 +35,10 @@ import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; -import haveno.core.util.coin.CoinUtil; -import haveno.core.xmr.wallet.Restrictions; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; import haveno.desktop.main.offer.MutableOfferDataModel; import haveno.network.p2p.P2PService; -import java.math.BigInteger; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -52,7 +50,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { DuplicateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, OfferUtil offerUtil, - XmrWalletService btcWalletService, + XmrWalletService xmrWalletService, Preferences preferences, User user, P2PService p2PService, @@ -65,7 +63,7 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { super(createOfferService, openOfferManager, offerUtil, - btcWalletService, + xmrWalletService, preferences, user, p2PService, @@ -79,26 +77,25 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { public void populateData(Offer offer) { if (offer == null) return; - paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + + PaymentAccount account = user.getPaymentAccount(offer.getMakerPaymentAccountId()); + if (account != null) { + this.paymentAccount = account; + } setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - - setBuyerSecurityDeposit(getBuyerSecurityAsPercent(offer)); - if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } - } + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); + setSecurityDepositPct(getSecurityAsPercent(offer)); + setExtraInfo(offer.getOfferExtraInfo()); - private double getBuyerSecurityAsPercent(Offer offer) { - BigInteger offerBuyerSecurityDeposit = getBoundedBuyerSecurityDeposit(offer.getMaxBuyerSecurityDeposit()); - double offerBuyerSecurityDepositAsPercent = CoinUtil.getAsPercentPerBtc(offerBuyerSecurityDeposit, - offer.getAmount()); - return Math.min(offerBuyerSecurityDepositAsPercent, - Restrictions.getMaxBuyerSecurityDepositAsPercent()); + OpenOffer openOffer = openOfferManager.getOpenOffer(offer.getId()).orElse(null); + if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice()); } @Override 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 33285e7c32..7cb24ca3eb 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 @@ -70,6 +70,7 @@ public class DuplicateOfferView extends MutableOfferView 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + triggerFocusOutOnAmountFields(); onFocusOutPriceAsPercentageTextField(true, false); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index be2b811f07..530cad9c86 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -20,6 +20,7 @@ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; + import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; @@ -54,6 +55,7 @@ class EditOfferDataModel extends MutableOfferDataModel { private final CorePersistenceProtoResolver corePersistenceProtoResolver; private OpenOffer openOffer; private OpenOffer.State initialState; + private Offer editedOffer; @Inject EditOfferDataModel(CreateOfferService createOfferService, @@ -95,10 +97,10 @@ class EditOfferDataModel extends MutableOfferDataModel { price.set(null); volume.set(null); minVolume.set(null); - buyerSecurityDepositPct.set(0); + securityDepositPct.set(0); paymentAccounts.clear(); paymentAccount = null; - marketPriceMargin = 0; + marketPriceMarginPct = 0; } public void applyOpenOffer(OpenOffer openOffer) { @@ -127,20 +129,22 @@ class EditOfferDataModel extends MutableOfferDataModel { // If the security deposit got bounded because it was below the coin amount limit, it can be bigger // 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 buyerSecurityDepositPercent = CoinUtil.getAsPercentPerBtc(offer.getMaxBuyerSecurityDeposit(), offer.getAmount()); - if (buyerSecurityDepositPercent > Restrictions.getMaxBuyerSecurityDepositAsPercent() - && offer.getMaxBuyerSecurityDeposit().equals(Restrictions.getMinBuyerSecurityDeposit())) - buyerSecurityDepositPct.set(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + double securityDepositPercent = CoinUtil.getAsPercentPerXmr(offer.getMaxSellerSecurityDeposit(), offer.getAmount()); + if (securityDepositPercent > Restrictions.getMaxSecurityDepositAsPercent() + && offer.getMaxSellerSecurityDeposit().equals(Restrictions.getMinSecurityDeposit())) + securityDepositPct.set(Restrictions.getDefaultSecurityDepositAsPercent()); else - buyerSecurityDepositPct.set(buyerSecurityDepositPercent); + securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; + + triggerPrice = openOffer.getTriggerPrice(); + extraInfo.set(offer.getOfferExtraInfo()); } - @Override public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { try { - return super.initWithData(direction, tradeCurrency); + return super.initWithData(direction, tradeCurrency, false); } catch (NullPointerException e) { if (e.getMessage().contains("tradeCurrency")) { throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); @@ -162,10 +166,11 @@ class EditOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setTriggerPrice(openOffer.getTriggerPrice()); + setExtraInfo(offer.getOfferExtraInfo()); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { @@ -211,21 +216,25 @@ class EditOfferDataModel extends MutableOfferDataModel { offerPayload.getLowerClosePrice(), offerPayload.getUpperClosePrice(), offerPayload.isPrivateOffer(), - offerPayload.getHashOfChallenge(), + offerPayload.getChallengeHash(), offerPayload.getExtraDataMap(), offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), offerPayload.getArbitratorSignature(), - offerPayload.getReserveTxKeyImages()); + offerPayload.getReserveTxKeyImages(), + newOfferPayload.getExtraInfo()); - final Offer editedOffer = new Offer(editedPayload); + editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(Offer.State.AVAILABLE); openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { + resultHandler.handleResult(); // process result before nullifying state openOffer = null; - resultHandler.handleResult(); - }, errorMessageHandler); + editedOffer = null; + }, (errorMsg) -> { + errorMessageHandler.handleErrorMessage(errorMsg); + }); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { @@ -234,6 +243,15 @@ class EditOfferDataModel extends MutableOfferDataModel { }, errorMessageHandler); } + public boolean hasConflictingClone() { + Optional editedOpenOffer = openOfferManager.getOpenOffer(openOffer.getId()); + if (!editedOpenOffer.isPresent()) { + log.warn("Edited open offer is no longer present"); + return false; + } + return openOfferManager.hasConflictingClone(editedOpenOffer.get()); + } + @Override protected Set getUserPaymentAccounts() { throw new RuntimeException("Edit offer not supported with XMR"); 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 60aa3c4dec..8b1d9775e6 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 @@ -19,6 +19,8 @@ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.util.Tuple4; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.Res; @@ -90,6 +92,7 @@ public class EditOfferView extends MutableOfferView { addBindings(); hideOptionsGroup(); + hideNextButtons(); // Lock amount field as it would require bigger changes to support increased amount values. amountTextField.setDisable(true); @@ -139,6 +142,7 @@ public class EditOfferView extends MutableOfferView { initWithData(openOffer.getOffer().getDirection(), CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + false, null); model.onStartEditOffer(errorMessage -> { @@ -178,7 +182,7 @@ public class EditOfferView extends MutableOfferView { private void addConfirmEditGroup() { - int tmpGridRow = 4; + int tmpGridRow = 6; final Tuple4 editOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("editOffer.confirmEdit")); final HBox editOfferConfirmationBox = editOfferTuple.fourth; @@ -204,25 +208,34 @@ public class EditOfferView extends MutableOfferView { cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); - //edit offer + + // edit offer model.onPublishOffer(() -> { - String key = "editOfferSuccess"; - if (DontShowAgainLookup.showAgain(key)) { - new Popup() - .feedback(Res.get("editOffer.success")) - .dontShowAgainId(key) - .show(); + if (model.dataModel.hasConflictingClone()) { + new Popup().warning(Res.get("editOffer.hasConflictingClone")).show(); + } else { + String key = "editOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("editOffer.success")) + .dontShowAgainId(key) + .show(); + } } - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - close(); + UserThread.execute(() -> { + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }); }, (message) -> { - log.error(message); - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - model.isNextButtonDisabled.setValue(false); - cancelButton.setDisable(false); - new Popup().warning(Res.get("editOffer.failed", message)).show(); + UserThread.execute(() -> { + log.error(message); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(Res.get("editOffer.failed", message)).show(); + }); }); } }); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 721f21bbfc..53febf4dc5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -44,7 +44,7 @@ class EditOfferViewModel extends MutableOfferViewModel { FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals priceValidator4Decimals, AmountValidator8Decimals priceValidator8Decimals, - XmrValidator btcValidator, + XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, @@ -56,7 +56,7 @@ class EditOfferViewModel extends MutableOfferViewModel { fiatVolumeValidator, priceValidator4Decimals, priceValidator8Decimals, - btcValidator, + xmrValidator, securityDepositValidator, priceFeedService, accountAgeWitnessService, @@ -111,7 +111,7 @@ class EditOfferViewModel extends MutableOfferViewModel { } public boolean isSecurityDepositValid() { - return securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + return securityDepositValidator.validate(securityDeposit.get()).isValid; } @Override 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 9c351e6272..35fe31dc22 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 @@ -27,6 +27,7 @@ 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; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableViewAndModel; import haveno.desktop.common.view.FxmlView; @@ -98,15 +99,18 @@ public class FailedTradesView extends ActivatableViewAndModel filterTextFieldListener; private Scene scene; private XmrWalletService xmrWalletService; + private User user; private ContextMenu contextMenu; @Inject public FailedTradesView(FailedTradesViewModel model, TradeDetailsWindow tradeDetailsWindow, - XmrWalletService xmrWalletService) { + XmrWalletService xmrWalletService, + User user) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; this.xmrWalletService = xmrWalletService; + this.user = user; } @Override @@ -190,9 +194,33 @@ public class FailedTradesView extends ActivatableViewAndModel { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.maker"), + selectedFailedTrade.getMaker().getSecurityDeposit(), + selectedFailedTrade.getMaker().getReserveTxHash(), + selectedFailedTrade.getMaker().getReserveTxHex()); + }); + + item2.setOnAction(event -> { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.taker"), + selectedFailedTrade.getTaker().getSecurityDeposit(), + selectedFailedTrade.getTaker().getReserveTxHash(), + selectedFailedTrade.getTaker().getReserveTxHex()); + }); + + contextMenu.getItems().addAll(item1, item2); + } tableView.setRowFactory(tv -> { TableRow row = new TableRow<>(); @@ -202,26 +230,6 @@ public class FailedTradesView extends ActivatableViewAndModel { - Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); - handleContextMenu("portfolio.failed.penalty.msg", - Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), - Res.get("shared.maker"), - selectedFailedTrade.getMaker().getSecurityDeposit(), - selectedFailedTrade.getMaker().getReserveTxHash(), - selectedFailedTrade.getMaker().getReserveTxHex()); - }); - - item2.setOnAction(event -> { - Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); - handleContextMenu("portfolio.failed.penalty.msg", - Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), - Res.get("shared.taker"), - selectedFailedTrade.getTaker().getSecurityDeposit(), - selectedFailedTrade.getTaker().getReserveTxHash(), - selectedFailedTrade.getTaker().getReserveTxHex()); - }); - numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); @@ -357,7 +365,7 @@ public class FailedTradesView extends ActivatableViewAndModel o2.getOffer().getDate().compareTo(o1.getOffer().getDate())); } - boolean wasTriggered(OpenOffer openOffer) { - return TriggerPriceService.wasTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer); + boolean isTriggered(OpenOffer openOffer) { + return TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), 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 30da4d0c19..035ec5fbdc 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 @@ -42,7 +42,8 @@ - + + @@ -50,11 +51,13 @@ - + + + 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 c1f5566398..3282ab078a 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 @@ -22,8 +22,8 @@ import com.googlecode.jcsv.writer.CSVEntryConverter; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.core.locale.Res; import haveno.core.offer.Offer; -import haveno.core.offer.OfferPayload; import haveno.core.offer.OpenOffer; +import haveno.core.offer.OpenOfferManager; import haveno.core.user.DontShowAgainLookup; import haveno.desktop.Navigation; import haveno.desktop.common.view.ActivatableViewAndModel; @@ -40,8 +40,9 @@ import haveno.desktop.main.funds.withdrawal.WithdrawalView; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.portfolio.PortfolioView; -import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; +import haveno.desktop.main.portfolio.presentation.PortfolioUtil; import static haveno.desktop.util.FormBuilder.getRegularIconButton; +import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import java.util.Comparator; import java.util.HashMap; @@ -51,13 +52,11 @@ import java.util.stream.Collectors; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.collections.ObservableList; +import javafx.collections.ListChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; @@ -73,6 +72,7 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; import org.jetbrains.annotations.NotNull; @@ -80,12 +80,39 @@ import org.jetbrains.annotations.NotNull; @FxmlView public class OpenOffersView extends ActivatableViewAndModel { + private enum ColumnNames { + OFFER_ID(Res.get("shared.offerId")), + GROUP_ID(Res.get("openOffer.header.groupId")), + DATE(Res.get("shared.dateTime")), + MARKET(Res.get("shared.market")), + PRICE(Res.get("shared.price")), + DEVIATION(Res.get("shared.deviation")), + TRIGGER_PRICE(Res.get("openOffer.header.triggerPrice")), + AMOUNT(Res.get("shared.XMRMinMax")), + VOLUME(Res.get("shared.amountMinMax")), + PAYMENT_METHOD(Res.get("shared.paymentMethod")), + DIRECTION(Res.get("shared.offerType")), + STATUS(Res.get("shared.state")); + + private final String text; + + ColumnNames(String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + } + @FXML TableView tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, - marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, - removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn; + marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupIdColumn, + removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn, + cloneItemColumn; @FXML HBox searchBox; @FXML @@ -108,37 +135,48 @@ public class OpenOffersView extends ActivatableViewAndModel sortedList; private FilteredList filteredList; private ChangeListener filterTextFieldListener; - private PortfolioView.OpenOfferActionHandler openOfferActionHandler; + private final OpenOfferManager openOfferManager; + private PortfolioView.EditOpenOfferHandler editOpenOfferHandler; + private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler; private ChangeListener widthListener; + private ListChangeListener sortedListChangedListener; private Map> offerStateChangeListeners = new HashMap>(); @Inject - public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { + public OpenOffersView(OpenOffersViewModel model, + OpenOfferManager openOfferManager, + Navigation navigation, + OfferDetailsWindow offerDetailsWindow) { super(model); this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; + this.openOfferManager = openOfferManager; } @Override public void initialize() { widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); - paymentMethodColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.paymentMethod"))); - priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price"))); - deviationColumn.setGraphic(new AutoTooltipTableColumn<>(Res.get("shared.deviation"), + groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString())); + paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); + priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); + deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); - amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.XMRMinMax"))); - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountMinMax"))); - marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market"))); - directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); - dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); - offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId"))); - triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("openOffer.header.triggerPrice"))); - deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled"))); + triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString())); + amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); + volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); + marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); + directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DIRECTION.toString())); + dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString())); + offerIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_ID.toString())); + deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString())); editItemColumn.setGraphic(new AutoTooltipLabel("")); + duplicateItemColumn.setText(""); + cloneItemColumn.setText(""); removeItemColumn.setGraphic(new AutoTooltipLabel("")); setOfferIdColumnCellFactory(); + setGroupIdCellFactory(); setDirectionColumnCellFactory(); setMarketColumnCellFactory(); setPriceColumnCellFactory(); @@ -151,12 +189,15 @@ public class OpenOffersView extends ActivatableViewAndModel o.getOffer().getId())); + groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash() == null ? "" : o.getOpenOffer().getReserveTxHash())); directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); @@ -168,23 +209,21 @@ public class OpenOffersView extends ActivatableViewAndModel o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); - dateColumn.setSortType(TableColumn.SortType.DESCENDING); + dateColumn.setSortType(TableColumn.SortType.ASCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); - MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - editItem.setOnAction((event) -> { - try { - OfferPayload offerPayload = row.getItem().getOffer().getOfferPayload(); - navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class); - } catch (NullPointerException e) { - log.warn("Unable to get offerPayload - {}", e.toString()); - } - }); - rowMenu.getItems().add(editItem); + + MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); + rowMenu.getItems().add(duplicateOfferMenuItem); + + MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer")); + cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem())); + rowMenu.getItems().add(cloneOfferMenuItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -207,6 +246,15 @@ public class OpenOffersView extends ActivatableViewAndModel { + c.next(); + if (c.wasAdded() || c.wasRemoved()) { + updateNumberOfOffers(); + updateGroupIdColumnVisibility(); + updateTriggerColumnVisibility(); + } + }; } @Override @@ -214,8 +262,11 @@ public class OpenOffersView extends ActivatableViewAndModel(model.getList()); sortedList = new SortedList<>(filteredList); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + sortedList.addListener(sortedListChangedListener); tableView.setItems(sortedList); + updateGroupIdColumnVisibility(); + updateTriggerColumnVisibility(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { @@ -231,37 +282,27 @@ public class OpenOffersView extends ActivatableViewAndModel { - ObservableList> tableColumns = tableView.getColumns(); - int reportColumns = tableColumns.size() - 3; // CSV report excludes the last columns (icons) CSVEntryConverter headerConverter = item -> { - String[] columns = new String[reportColumns]; - for (int i = 0; i < columns.length; i++) { - Node graphic = tableColumns.get(i).getGraphic(); - if (graphic instanceof AutoTooltipLabel) { - columns[i] = ((AutoTooltipLabel) graphic).getText(); - } else if (graphic instanceof HBox) { - // Deviation has a Hbox with AutoTooltipLabel as first child in header - columns[i] = ((AutoTooltipLabel) ((Parent) graphic).getChildrenUnmodifiable().get(0)).getText(); - } else { - // Not expected - columns[i] = "N/A"; - } + String[] columns = new String[ColumnNames.values().length]; + for (ColumnNames m : ColumnNames.values()) { + columns[m.ordinal()] = m.toString(); } return columns; }; CSVEntryConverter contentConverter = item -> { - String[] columns = new String[reportColumns]; - columns[0] = model.getOfferId(item); - columns[1] = model.getDate(item); - columns[2] = model.getMarketLabel(item); - columns[3] = model.getPrice(item); - columns[4] = model.getPriceDeviation(item); - columns[5] = model.getTriggerPrice(item); - columns[6] = model.getAmount(item); - columns[7] = model.getVolume(item); - columns[8] = model.getPaymentMethod(item); - columns[9] = model.getDirectionLabel(item); - columns[10] = String.valueOf(!item.getOpenOffer().isDeactivated()); + String[] columns = new String[ColumnNames.values().length]; + columns[ColumnNames.OFFER_ID.ordinal()] = model.getOfferId(item); + columns[ColumnNames.GROUP_ID.ordinal()] = openOfferManager.hasClonedOffer(item.getOffer().getId()) ? getShortenedGroupId(item.getGroupId()) : ""; + columns[ColumnNames.DATE.ordinal()] = model.getDate(item); + columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item); + columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item); + columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item); + columns[ColumnNames.TRIGGER_PRICE.ordinal()] = model.getTriggerPrice(item); + columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item); + columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item); + columns[ColumnNames.PAYMENT_METHOD.ordinal()] = model.getPaymentMethod(item); + columns[ColumnNames.DIRECTION.ordinal()] = model.getDirectionLabel(item); + columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -280,9 +321,24 @@ public class OpenOffersView extends ActivatableViewAndModel item.getOpenOffer().getTriggerPrice()) + .sum() > 0); + } + @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); + sortedList.removeListener(sortedListChangedListener); exportButton.setOnAction(null); filterTextField.textProperty().removeListener(filterTextFieldListener); @@ -320,31 +376,34 @@ public class OpenOffersView extends ActivatableViewAndModel 1200); + triggerPriceColumn.setVisible(width > 1300); } private void onDeactivateOpenOffer(OpenOffer openOffer) { @@ -361,14 +420,14 @@ public class OpenOffersView extends ActivatableViewAndModel log.debug("Deactivate offer was successful"), (message) -> { log.error(message); - new Popup().warning(Res.get("offerbook.deactivateOffer.failed", message)).show(); + new Popup().warning(message).show(); }); updateSelectToggleButtonState(); } } private void onActivateOpenOffer(OpenOffer openOffer) { - if (model.isBootstrappedOrShowPopup() && !model.dataModel.wasTriggered(openOffer)) { + if (model.isBootstrappedOrShowPopup() && !model.dataModel.isTriggered(openOffer)) { model.onActivateOpenOffer(openOffer, () -> log.debug("Activate offer was successful"), (message) -> { @@ -397,12 +456,18 @@ public class OpenOffersView extends ActivatableViewAndModel { log.debug("Remove offer was successful"); tableView.refresh(); + // We do not show the popup if it's a cloned offer with shared maker reserve tx + if (hasClonedOffer) { + return; + } + String key = "WithdrawFundsAfterRemoveOfferInfo"; if (DontShowAgainLookup.showAgain(key)) { new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) @@ -420,10 +485,46 @@ public class OpenOffersView extends ActivatableViewAndModel doCloneOffer(item)) + .show(); + } else { + doCloneOffer(item); + } + } + } + + private void doCloneOffer(OpenOfferListItem item) { + OpenOffer openOffer = item.getOpenOffer(); + if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload() == null) { + return; + } + cloneOpenOfferHandler.onCloneOpenOffer(openOffer); + } + private void setOfferIdColumnCellFactory() { offerIdColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); offerIdColumn.getStyleClass().addAll("number-column", "first-column"); @@ -434,21 +535,28 @@ public class OpenOffersView extends ActivatableViewAndModel call(TableColumn column) { return new TableCell<>() { - private HyperlinkWithIcon field; + private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - field = new HyperlinkWithIcon(model.getOfferId(item)); - field.setOnAction(event -> offerDetailsWindow.show(item.getOffer())); - field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); - setGraphic(field); + hyperlinkWithIcon = new HyperlinkWithIcon(item.getOffer().getShortId()); + if (model.isDeactivated(item)) { + // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon;-( + hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); + hyperlinkWithIcon.getIcon().setOpacity(0.2); + } + hyperlinkWithIcon.setOnAction(event -> { + offerDetailsWindow.show(item.getOffer()); + }); + + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); + setGraphic(hyperlinkWithIcon); } else { setGraphic(null); - if (field != null) - field.setOnAction(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); } } }; @@ -456,6 +564,55 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + groupIdColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + + return new TableCell<>() { + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + Label label; + Text icon; + if (openOfferManager.hasClonedOffer(item.getOpenOffer().getId())) { + label = new Label(getShortenedGroupId(item.getOpenOffer().getGroupId())); + icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon"); + icon.setVisible(true); + setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); + } else { + label = new Label(""); + icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon"); + icon.setVisible(false); + setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getOpenOffer().getReserveTxHash()))); + } + + if (model.isDeactivated(item)) { + getStyleClass().add("offer-disabled"); + icon.setOpacity(0.2); + } + setGraphic(label); + } else { + setGraphic(null); + } + } + }; + } + }); + } + + private String getShortenedGroupId(String groupId) { + if (groupId.length() > 5) { + return groupId.substring(0, 5); + } + return groupId; + } + private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); dateColumn.setCellFactory( @@ -692,7 +849,7 @@ public class OpenOffersView extends ActivatableViewAndModel { if (openOffer.isDeactivated()) { onActivateOpenOffer(openOffer); @@ -779,6 +936,74 @@ public class OpenOffersView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + duplicateItemColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); + button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis"))); + setGraphic(button); + } + button.setOnAction(event -> onDuplicateOffer(item)); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + + private void setCloneColumnCellFactory() { + cloneItemColumn.getStyleClass().add("avatar-column"); + cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + cloneItemColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW); + button.setTooltip(new Tooltip(Res.get("offerbook.cloneOffer"))); + setGraphic(button); + } + button.setOnAction(event -> onCloneOffer(item)); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + private void setTriggerIconColumnCellFactory() { triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); triggerIconColumn.setCellFactory( @@ -798,7 +1023,7 @@ public class OpenOffersView extends ActivatableViewAndModel 0; button.setVisible(triggerPriceSet); - if (model.dataModel.wasTriggered(item.getOpenOffer())) { + if (model.dataModel.isTriggered(item.getOpenOffer())) { button.getGraphic().getStyleClass().add("warning"); button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); } else { @@ -854,8 +1079,12 @@ public class OpenOffersView extends ActivatableViewAndModel return item.getOffer().getShortId(); } + String getGroupId(OpenOfferListItem item) { + return item.getGroupId(); + } + String getAmount(OpenOfferListItem item) { return (item != null) ? DisplayUtils.formatAmount(item.getOffer(), btcFormatter) : ""; } @@ -121,7 +125,7 @@ class OpenOffersViewModel extends ActivatableWithDataModel if ((item == null)) return ""; - return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode()); + return DisplayUtils.getDirectionWithCode(dataModel.getDirection(item.getOffer()), item.getOffer().getCurrencyCode(), item.getOffer().isPrivateOffer()); } String getMarketLabel(OpenOfferListItem item) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 944c977b11..ea93d145fb 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -332,15 +332,17 @@ public class PendingTradesDataModel extends ActivatableDataModel { } // add shown trades to list - list.clear(); - list.addAll(tradeManager.getObservableList().stream() - .filter(trade -> isTradeShown(trade)) - .map(trade -> new PendingTradesListItem(trade, btcFormatter)) - .collect(Collectors.toList())); - } + synchronized (list) { + list.clear(); + list.addAll(tradeManager.getObservableList().stream() + .filter(trade -> isTradeShown(trade)) + .map(trade -> new PendingTradesListItem(trade, btcFormatter)) + .collect(Collectors.toList())); - // we sort by date, earliest first - list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); + // we sort by date, earliest first + list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); + } + } selectBestItem(); } @@ -350,17 +352,21 @@ public class PendingTradesDataModel extends ActivatableDataModel { } private void selectBestItem() { - if (list.size() == 1) - doSelectItem(list.get(0)); - else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) - doSelectItem(list.get(0)); - else if (list.size() == 0) - doSelectItem(null); + synchronized (list) { + if (list.size() == 1) + doSelectItem(list.get(0)); + else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) + doSelectItem(list.get(0)); + else if (list.size() == 0) + doSelectItem(null); + } } private void selectItemByTradeId(String tradeId) { if (activated) { - list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); + synchronized (list) { + list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); + } } } @@ -380,14 +386,11 @@ public class PendingTradesDataModel extends ActivatableDataModel { tradeStateChangeListener = (observable, oldValue, newValue) -> { String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); + if (makerDepositTxHash != null || takerDepositTxHash != null) { notificationCenter.setSelectedTradeId(tradeId); selectedTrade.stateProperty().removeListener(tradeStateChangeListener); - } else { - makerTxId.set(""); - takerTxId.set(""); } }; selectedTrade.stateProperty().addListener(tradeStateChangeListener); @@ -401,13 +404,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { isMaker = tradeManager.isMyOffer(offer); String makerDepositTxHash = selectedTrade.getMaker().getDepositTxHash(); String takerDepositTxHash = selectedTrade.getTaker().getDepositTxHash(); - if (makerDepositTxHash != null && takerDepositTxHash != null) { - makerTxId.set(makerDepositTxHash); - takerTxId.set(takerDepositTxHash); - } else { - makerTxId.set(""); - takerTxId.set(""); - } + makerTxId.set(nullToEmptyString(makerDepositTxHash)); + takerTxId.set(nullToEmptyString(takerDepositTxHash)); notificationCenter.setSelectedTradeId(tradeId); } else { selectedTrade = null; @@ -419,6 +417,10 @@ public class PendingTradesDataModel extends ActivatableDataModel { }); } + private String nullToEmptyString(String str) { + return str == null ? "" : str; + } + private void tryOpenDispute(boolean isSupportTicket) { Trade trade = getTrade(); if (trade == null) { @@ -446,7 +448,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { } depositTxId = trade.getMaker().getDepositTxHash(); } else { - if (trade.getTaker().getDepositTxHash() == null) { + if (trade.getTaker().getDepositTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("Deposit tx must not be null"); new Popup().instruction(Res.get("portfolio.pending.error.depositTxNull")).show(); return; @@ -548,10 +550,10 @@ public class PendingTradesDataModel extends ActivatableDataModel { sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); } else if (useArbitration) { - // Only if we have completed mediation we allow arbitration disputeManager = arbitrationManager; Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); - trade.exportMultisigHex(); + + // send dispute opened message sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); } else { 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 8e47d0e07e..3b9a399e82 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 @@ -83,6 +83,15 @@ public class PendingTradesListItem implements FilterableListItem { if (StringUtils.containsIgnoreCase(getMarketDescription(), filterString)) { return true; } + if (StringUtils.containsIgnoreCase(getTrade().getOffer().getCombinedExtraInfo(), filterString)) { + return true; + } + if (getTrade().getBuyer().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getBuyer().getPaymentAccountPayload().getPaymentDetails(), filterString)) { + return true; + } + if (getTrade().getSeller().getPaymentAccountPayload() != null && StringUtils.containsIgnoreCase(getTrade().getSeller().getPaymentAccountPayload().getPaymentDetails(), filterString)) { + return true; + } return StringUtils.containsIgnoreCase(getPriceAsString(), filterString); } } 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 5157c9b4c5..645fbb5014 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 @@ -363,7 +363,11 @@ public class PendingTradesView extends ActivatableViewAndModel moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade())))); + UserThread.execute(() -> { + synchronized (model.dataModel.list) { + moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade()))); + } + }); } private boolean isMaybeInvalidTrade(Trade trade) { @@ -403,8 +407,8 @@ public class PendingTradesView extends ActivatableViewAndModel { - Trade trade = t.getTrade(); - synchronized (trade.getChatMessages()) { - newChatMessagesByTradeMap.put(trade.getId(), - trade.getChatMessages().stream() - .filter(m -> !m.isWasDisplayed()) - .filter(m -> !m.isSystemMessage()) - .count()); - } - }); + synchronized (model.dataModel.list) { + model.dataModel.list.forEach(t -> { + Trade trade = t.getTrade(); + synchronized (trade.getChatMessages()) { + newChatMessagesByTradeMap.put(trade.getId(), + trade.getChatMessages().stream() + .filter(m -> !m.isWasDisplayed()) + .filter(m -> !m.isSystemMessage()) + .count()); + } + }); + } } private void openChat(Trade trade) { 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 41881c58be..6a34504dad 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 @@ -100,7 +100,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel buyerState = new SimpleObjectProperty<>(); private final ObjectProperty sellerState = new SimpleObjectProperty<>(); @Getter - private final ObjectProperty messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private final ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; private Subscription paymentAccountDecryptedSubscription; private Subscription payoutStateSubscription; @@ -186,7 +186,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onPayoutStateChanged(state); }); - messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStateProperty(), this::onMessageStateChanged); + messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged); } } } @@ -215,8 +215,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel labelSelfTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), - Layout.COMPACT_FIRST_ROW_DISTANCE); + boolean showSelfTxId = model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showSelfTxId) { + final Tuple3 labelSelfTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, gridRow, Res.get("shared.yourDepositTransactionId"), + Layout.COMPACT_FIRST_ROW_DISTANCE); - GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); - selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; + GridPane.setColumnSpan(labelSelfTxIdTextFieldVBoxTuple3.third, 2); + selfTxIdTextField = labelSelfTxIdTextFieldVBoxTuple3.second; - String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); - if (!selfTxId.isEmpty()) - selfTxIdTextField.setup(selfTxId, trade); - else - selfTxIdTextField.cleanup(); + String selfTxId = model.dataModel.isMaker() ? model.dataModel.makerTxId.get() : model.dataModel.takerTxId.get(); + if (!selfTxId.isEmpty()) + selfTxIdTextField.setup(selfTxId, trade); + else + selfTxIdTextField.cleanup(); + } // peer's deposit tx id - final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = - addTopLabelTxIdTextField(gridPane, ++gridRow, Res.get("shared.peerDepositTransactionId"), - -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR); + boolean showPeerTxId = !model.dataModel.isMaker() || !trade.hasBuyerAsTakerWithoutDeposit(); + if (showPeerTxId) { + final Tuple3 labelPeerTxIdTextFieldVBoxTuple3 = + addTopLabelTxIdTextField(gridPane, showSelfTxId ? ++gridRow : gridRow, Res.get("shared.peerDepositTransactionId"), + showSelfTxId ? -Layout.GROUP_DISTANCE_WITHOUT_SEPARATOR : Layout.COMPACT_FIRST_ROW_DISTANCE); - GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); - peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; + GridPane.setColumnSpan(labelPeerTxIdTextFieldVBoxTuple3.third, 2); + peerTxIdTextField = labelPeerTxIdTextFieldVBoxTuple3.second; - String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); - if (!peerTxId.isEmpty()) - peerTxIdTextField.setup(peerTxId, trade); - else - peerTxIdTextField.cleanup(); + String peerTxId = model.dataModel.isMaker() ? model.dataModel.takerTxId.get() : model.dataModel.makerTxId.get(); + if (!peerTxId.isEmpty()) + peerTxIdTextField.setup(peerTxId, trade); + else + peerTxIdTextField.cleanup(); + } if (model.dataModel.getTrade() != null) { checkNotNull(model.dataModel.getTrade().getOffer(), "Offer must not be null in TradeStepView"); @@ -399,6 +405,7 @@ public abstract class TradeStepView extends AnchorPane { } private void updateTimeLeft() { + if (!trade.isInitialized()) return; if (timeLeftTextField != null) { // TODO (woodser): extra TradeStepView created but not deactivated on trade.setState(), so deactivate when model's trade is null @@ -648,7 +655,7 @@ public abstract class TradeStepView extends AnchorPane { model.dataModel.onMoveInvalidTradeToFailedTrades(trade); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); // TODO (woodser): separate error messages for maker/taker return; - } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null) { + } else if (trade instanceof TakerTrade && trade.getTakerDepositTx() == null && !trade.hasBuyerAsTakerWithoutDeposit()) { log.error("trade.getTakerDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); //model.dataModel.addTradeToFailedTrades(); 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 804a6dd741..4888d5b4a8 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 @@ -51,7 +51,7 @@ public class BuyerStep1View extends TradeStepView { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step1.info", Res.get("shared.You")); + return Res.get("portfolio.pending.step1.info.you"); } /////////////////////////////////////////////////////////////////////////////////////////// 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 e36cba49d4..51a36488eb 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 @@ -55,6 +55,7 @@ import haveno.desktop.components.paymentmethods.CashAppForm; import haveno.desktop.components.paymentmethods.CashAtAtmForm; import haveno.desktop.components.paymentmethods.PayByMailForm; import haveno.desktop.components.paymentmethods.PayPalForm; +import haveno.desktop.components.paymentmethods.PaysafeForm; import haveno.desktop.components.paymentmethods.CashDepositForm; import haveno.desktop.components.paymentmethods.CelPayForm; import haveno.desktop.components.paymentmethods.ChaseQuickPayForm; @@ -326,7 +327,7 @@ public class BuyerStep2View extends TradeStepView { 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.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0); + gridRow = F2FForm.addStep2Form(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true); break; case PaymentMethod.BLOCK_CHAINS_ID: case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: @@ -417,6 +418,9 @@ public class BuyerStep2View extends TradeStepView { case PaymentMethod.VENMO_ID: gridRow = VenmoForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); break; + case PaymentMethod.PAYSAFE_ID: + gridRow = PaysafeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload); + break; default: log.error("Not supported PaymentMethod: " + paymentMethodId); } 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 57fda229f3..dba3fe4a3d 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 @@ -52,7 +52,7 @@ public class BuyerStep3View extends TradeStepView { public void activate() { super.activate(); - model.getMessageStateProperty().addListener(messageStateChangeListener); + model.getPaymentSentMessageStateProperty().addListener(messageStateChangeListener); updateMessageStateInfo(); } @@ -60,7 +60,7 @@ public class BuyerStep3View extends TradeStepView { public void deactivate() { super.deactivate(); - model.getMessageStateProperty().removeListener(messageStateChangeListener); + model.getPaymentSentMessageStateProperty().removeListener(messageStateChangeListener); } @@ -87,7 +87,7 @@ public class BuyerStep3View extends TradeStepView { } private void updateMessageStateInfo() { - MessageState messageState = model.getMessageStateProperty().get(); + MessageState messageState = model.getPaymentSentMessageStateProperty().get(); textFieldWithIcon.setText(Res.get("message.state." + messageState.name())); Label iconLabel = textFieldWithIcon.getIconLabel(); switch (messageState) { @@ -112,6 +112,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 34bfc75223..e589443301 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 @@ -79,7 +79,7 @@ public class BuyerStep4View extends TradeStepView { TitledGroupBg completedTradeLabel = new TitledGroupBg(); if (trade.getDisputeState().isMediated()) { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.mediated")); - } else if (trade.getDisputeState().isArbitrated() && trade.getDisputes().get(0).getDisputeResultProperty().get() != null) { + } else if (trade.getDisputeState().isArbitrated() && trade.getDisputeResult() != null) { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.arbitrated")); } else { completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle")); @@ -90,8 +90,8 @@ public class BuyerStep4View extends TradeStepView { gridPane.getChildren().add(hBox2); GridPane.setRowSpan(hBox2, 5); - if (trade.getDisputeState().isNotDisputed()) { - addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE); + if (trade.isPaymentReceived()) { + addCompactTopLabelTextField(gridPane, gridRow, getXmrTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, getTraditionalTradeAmountLabel(), model.getFiatVolume()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.refunded"), model.getSecurityDeposit()); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.tradeFee"), model.getTradeFee()); @@ -149,7 +149,7 @@ public class BuyerStep4View extends TradeStepView { } } - protected String getBtcTradeAmountLabel() { + protected String getXmrTradeAmountLabel() { return Res.get("portfolio.pending.step5_buyer.bought"); } 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 f641ddf3f4..2ca41f1d42 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 @@ -48,7 +48,7 @@ public class SellerStep1View extends TradeStepView { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step1.info", Res.get("shared.TheXMRBuyer")); + return Res.get("portfolio.pending.step1.info.buyer"); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java index 06ceba45a2..e9905c3f82 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java @@ -46,8 +46,8 @@ public class SellerStep2View extends TradeStepView { if (model.dataModel.getSellersPaymentAccountPayload() instanceof F2FAccountPayload) { addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("portfolio.pending.step2_seller.f2fInfo.headline"), Layout.COMPACT_GROUP_DISTANCE); - gridRow = F2FForm.addFormForBuyer(gridPane, --gridRow, model.dataModel.getSellersPaymentAccountPayload(), - model.dataModel.getTrade().getOffer(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); + gridRow = F2FForm.addStep2Form(gridPane, --gridRow, model.dataModel.getSellersPaymentAccountPayload(), + model.dataModel.getTrade().getOffer(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, false); } } 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 a492c74423..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 @@ -135,6 +135,7 @@ public class SellerStep3View extends TradeStepView { statusLabel.setText(Res.get("shared.messageStoredInMailbox")); break; case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: + case BUYER_RECEIVED_PAYMENT_RECEIVED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageArrived")); break; @@ -150,6 +151,9 @@ public class SellerStep3View extends TradeStepView { break; } } + + // update confirm button state + confirmButton.setDisable(!confirmPaymentReceivedPermitted()); }); } @@ -203,10 +207,8 @@ public class SellerStep3View extends TradeStepView { .orElse(""); if (myPaymentAccountPayload instanceof AssetAccountPayload) { - if (myPaymentDetails.isEmpty()) { - // Not expected - myPaymentDetails = ((AssetAccountPayload) myPaymentAccountPayload).getAddress(); - } + // for crypto always display the receiving address + myPaymentDetails = ((AssetAccountPayload) myPaymentAccountPayload).getAddress(); peersPaymentDetails = peersPaymentAccountPayload != null ? ((AssetAccountPayload) peersPaymentAccountPayload).getAddress() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep4View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep4View.java index 82927e1d50..12bcf6317b 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep4View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep4View.java @@ -32,7 +32,7 @@ public class SellerStep4View extends BuyerStep4View { } @Override - protected String getBtcTradeAmountLabel() { + protected String getXmrTradeAmountLabel() { return Res.get("portfolio.pending.step5_seller.sold"); } diff --git a/desktop/src/main/java/haveno/desktop/main/presentation/MarketPricePresentation.java b/desktop/src/main/java/haveno/desktop/main/presentation/MarketPricePresentation.java index ab77b5e00d..3529369285 100644 --- a/desktop/src/main/java/haveno/desktop/main/presentation/MarketPricePresentation.java +++ b/desktop/src/main/java/haveno/desktop/main/presentation/MarketPricePresentation.java @@ -110,9 +110,19 @@ public class MarketPricePresentation { } private void fillPriceFeedComboBoxItems() { - List currencyItems = preferences.getTradeCurrenciesAsObservable() + + // collect unique currency code bases + List uniqueCurrencyCodeBases = preferences.getTradeCurrenciesAsObservable() .stream() - .map(tradeCurrency -> new PriceFeedComboBoxItem(tradeCurrency.getCode())) + .map(TradeCurrency::getCode) + .map(CurrencyUtil::getCurrencyCodeBase) + .distinct() + .collect(Collectors.toList()); + + // create price feed items + List currencyItems = uniqueCurrencyCodeBases + .stream() + .map(currencyCodeBase -> new PriceFeedComboBoxItem(currencyCodeBase)) .collect(Collectors.toList()); priceFeedComboBoxItems.setAll(currencyItems); } @@ -171,7 +181,7 @@ public class MarketPricePresentation { private Optional findPriceFeedComboBoxItem(String currencyCode) { return priceFeedComboBoxItems.stream() - .filter(item -> item.currencyCode.equals(currencyCode)) + .filter(item -> CurrencyUtil.getCurrencyCodeBase(item.currencyCode).equals(CurrencyUtil.getCurrencyCodeBase(currencyCode))) .findAny(); } diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java b/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java index 80bd094fc9..fea0ef6f34 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/MoneroNetworkListItem.java @@ -17,28 +17,23 @@ package haveno.desktop.main.settings.network; -import monero.daemon.model.MoneroPeer; +import haveno.core.locale.Res; +import monero.common.MoneroRpcConnection; public class MoneroNetworkListItem { - private final MoneroPeer peer; - - public MoneroNetworkListItem(MoneroPeer peer) { - this.peer = peer; + private final MoneroRpcConnection connection; + private final boolean connected; + + public MoneroNetworkListItem(MoneroRpcConnection connection, boolean connected) { + this.connection = connection; + this.connected = connected; } - public String getOnionAddress() { - return peer.getHost() + ":" + peer.getPort(); + public String getAddress() { + return connection.getUri(); } - public String getVersion() { - return ""; - } - - public String getSubVersion() { - return ""; - } - - public String getHeight() { - return String.valueOf(peer.getHeight()); + public String getConnected() { + return connected ? Res.get("settings.net.connected") : ""; } } 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 3e9d25428e..bbfb4c6e05 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 @@ -45,27 +45,17 @@ - - + + - + - + - + - - - - - - - - - - - + @@ -101,7 +91,7 @@ - + @@ -169,7 +159,10 @@ - + + + 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 931a6e3526..a767d032bc 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 @@ -44,7 +44,6 @@ import haveno.desktop.main.overlays.windows.TorNetworkSettingsWindow; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import haveno.network.p2p.network.Statistic; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static javafx.beans.binding.Bindings.createStringBinding; @@ -63,7 +62,6 @@ import javafx.scene.control.TextField; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.GridPane; -import monero.daemon.model.MoneroPeer; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -77,9 +75,9 @@ public class NetworkSettingsView extends ActivatableView { @FXML InputTextField xmrNodesInputTextField; @FXML - TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; + TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField, minVersionForTrading; @FXML - Label p2PPeersLabel, moneroPeersLabel; + Label p2PPeersLabel, moneroConnectionsLabel; @FXML RadioButton useTorForXmrAfterSyncRadio, useTorForXmrOffRadio, useTorForXmrOnRadio; @FXML @@ -87,13 +85,12 @@ public class NetworkSettingsView extends ActivatableView { @FXML TableView p2pPeersTableView; @FXML - TableView moneroPeersTableView; + TableView moneroConnectionsTableView; @FXML TableColumn onionAddressColumn, connectionTypeColumn, creationDateColumn, roundTripTimeColumn, sentBytesColumn, receivedBytesColumn, peerTypeColumn; @FXML - TableColumn moneroPeerAddressColumn, moneroPeerVersionColumn, - moneroPeerSubVersionColumn, moneroPeerHeightColumn; + TableColumn moneroConnectionAddressColumn, moneroConnectionConnectedColumn; @FXML Label rescanOutputsLabel; @FXML @@ -116,7 +113,7 @@ public class NetworkSettingsView extends ActivatableView { private final SortedList moneroSortedList = new SortedList<>(moneroNetworkListItems); private Subscription numP2PPeersSubscription; - private Subscription moneroPeersSubscription; + private Subscription moneroConnectionsSubscription; private Subscription moneroBlockHeightSubscription; private Subscription nodeAddressSubscription; private ChangeListener xmrNodesInputTextFieldFocusListener; @@ -156,17 +153,15 @@ public class NetworkSettingsView extends ActivatableView { p2pHeader.setText(Res.get("settings.net.p2pHeader")); onionAddress.setPromptText(Res.get("settings.net.onionAddressLabel")); xmrNodesLabel.setText(Res.get("settings.net.xmrNodesLabel")); - moneroPeersLabel.setText(Res.get("settings.net.moneroPeersLabel")); + moneroConnectionsLabel.setText(Res.get("settings.net.moneroPeersLabel")); useTorForXmrLabel.setText(Res.get("settings.net.useTorForXmrJLabel")); useTorForXmrAfterSyncRadio.setText(Res.get("settings.net.useTorForXmrAfterSyncRadio")); useTorForXmrOffRadio.setText(Res.get("settings.net.useTorForXmrOffRadio")); useTorForXmrOnRadio.setText(Res.get("settings.net.useTorForXmrOnRadio")); moneroNodesLabel.setText(Res.get("settings.net.moneroNodesLabel")); - moneroPeerAddressColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.onionAddressColumn"))); - moneroPeerAddressColumn.getStyleClass().add("first-column"); - moneroPeerVersionColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.versionColumn"))); - moneroPeerSubVersionColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.subVersionColumn"))); - moneroPeerHeightColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.heightColumn"))); + 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")); useCustomNodesRadio.setText(Res.get("settings.net.useCustomNodesRadio")); @@ -181,6 +176,7 @@ public class NetworkSettingsView extends ActivatableView { sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); + minVersionForTrading.setPromptText(Res.get("filterWindow.disableTradeBelowVersion")); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); @@ -192,19 +188,19 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsLabel.setVisible(false); rescanOutputsButton.setVisible(false); - GridPane.setMargin(moneroPeersLabel, new Insets(4, 0, 0, 0)); - GridPane.setValignment(moneroPeersLabel, VPos.TOP); + GridPane.setMargin(moneroConnectionsLabel, new Insets(4, 0, 0, 0)); + GridPane.setValignment(moneroConnectionsLabel, VPos.TOP); GridPane.setMargin(p2PPeersLabel, new Insets(4, 0, 0, 0)); GridPane.setValignment(p2PPeersLabel, VPos.TOP); - moneroPeersTableView.setMinHeight(180); - moneroPeersTableView.setPrefHeight(180); - moneroPeersTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - moneroPeersTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); - moneroPeersTableView.getSortOrder().add(moneroPeerAddressColumn); - moneroPeerAddressColumn.setSortType(TableColumn.SortType.ASCENDING); - + moneroConnectionAddressColumn.setSortType(TableColumn.SortType.ASCENDING); + moneroConnectionConnectedColumn.setSortType(TableColumn.SortType.DESCENDING); + moneroConnectionsTableView.setMinHeight(180); + moneroConnectionsTableView.setPrefHeight(180); + moneroConnectionsTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + moneroConnectionsTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); + moneroConnectionsTableView.getSortOrder().add(moneroConnectionConnectedColumn); p2pPeersTableView.setMinHeight(180); p2pPeersTableView.setPrefHeight(180); @@ -280,7 +276,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()) { @@ -303,17 +299,17 @@ public class NetworkSettingsView extends ActivatableView { moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener); if (filterManager.getFilter() != null) - applyPreventPublicXmrNetwork(); + applyFilter(); filterManager.filterProperty().addListener(filterPropertyListener); rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); - moneroPeersSubscription = EasyBind.subscribe(connectionService.peerConnectionsProperty(), - this::updateMoneroPeersTable); + moneroConnectionsSubscription = EasyBind.subscribe(connectionService.connectionsProperty(), + connections -> updateMoneroConnectionsTable()); moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(), - this::updateChainHeightTextField); + height -> updateMoneroConnectionsTable()); nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), nodeAddress -> onionAddress.setText(nodeAddress == null ? @@ -333,8 +329,8 @@ public class NetworkSettingsView extends ActivatableView { Statistic.numTotalReceivedMessagesPerSecProperty().get()), Statistic.numTotalReceivedMessagesPerSecProperty())); - moneroSortedList.comparatorProperty().bind(moneroPeersTableView.comparatorProperty()); - moneroPeersTableView.setItems(moneroSortedList); + moneroSortedList.comparatorProperty().bind(moneroConnectionsTableView.comparatorProperty()); + moneroConnectionsTableView.setItems(moneroSortedList); p2pSortedList.comparatorProperty().bind(p2pPeersTableView.comparatorProperty()); p2pPeersTableView.setItems(p2pSortedList); @@ -355,8 +351,8 @@ public class NetworkSettingsView extends ActivatableView { if (nodeAddressSubscription != null) nodeAddressSubscription.unsubscribe(); - if (moneroPeersSubscription != null) - moneroPeersSubscription.unsubscribe(); + if (moneroConnectionsSubscription != null) + moneroConnectionsSubscription.unsubscribe(); if (moneroBlockHeightSubscription != null) moneroBlockHeightSubscription.unsubscribe(); @@ -497,7 +493,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) { @@ -506,6 +504,10 @@ public class NetworkSettingsView extends ActivatableView { selectMoneroPeersToggle(); onMoneroPeersToggleSelected(false); } + + // set min version for trading + String minVersion = filterManager.getDisableTradeBelowVersion(); + minVersionForTrading.textProperty().setValue(minVersion == null ? Res.get("shared.none") : minVersion); } private boolean isPublicNodesDisabled() { @@ -513,21 +515,25 @@ public class NetworkSettingsView extends ActivatableView { } private void updateP2PTable() { - if (connectionService.isShutDownStarted()) return; // ignore if shutting down - p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); - p2pNetworkListItems.clear(); - p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() - .map(connection -> new P2pNetworkListItem(connection, clockWatcher)) - .collect(Collectors.toList())); + UserThread.execute(() -> { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down + p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); + p2pNetworkListItems.clear(); + p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() + .map(connection -> new P2pNetworkListItem(connection, clockWatcher)) + .collect(Collectors.toList())); + }); } - private void updateMoneroPeersTable(List peers) { - moneroNetworkListItems.clear(); - if (peers != null) { - moneroNetworkListItems.setAll(peers.stream() - .map(MoneroNetworkListItem::new) + private void updateMoneroConnectionsTable() { + UserThread.execute(() -> { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down + moneroNetworkListItems.clear(); + moneroNetworkListItems.setAll(connectionService.getConnections().stream() + .map(connection -> new MoneroNetworkListItem(connection, connection == connectionService.getConnection() && Boolean.TRUE.equals(connectionService.isConnected()))) .collect(Collectors.toList())); - } + updateChainHeightTextField(connectionService.chainHeightProperty().get()); + }); } private void updateChainHeightTextField(Number chainHeight) { 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 06331e5f1d..da71490b9a 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 @@ -187,7 +187,7 @@ public class PreferencesView extends ActivatableViewAndModel implements tooltip.setShowDuration(Duration.seconds(10)); filterTextField.setTooltip(tooltip); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); - HBox.setHgrow(filterTextField, Priority.NEVER); + HBox.setHgrow(filterTextField, Priority.ALWAYS); alertIconLabel = new Label(); Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel); @@ -927,6 +927,7 @@ public abstract class DisputeView extends ActivatableView implements buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); sellerOnionAddressColumn.setComparator(Comparator.comparing(this::getSellerOnionAddressColumnLabel)); marketColumn.setComparator((o1, o2) -> CurrencyUtil.getCurrencyPair(o1.getContract().getOfferPayload().getCurrencyCode()).compareTo(o2.getContract().getOfferPayload().getCurrencyCode())); + stateColumn.setComparator(Comparator.comparing(this::getDisputeStateText)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); @@ -1415,11 +1416,13 @@ public abstract class DisputeView extends ActivatableView implements private String getDisputeStateText(Dispute dispute) { Trade trade = tradeManager.getTrade(dispute.getTradeId()); if (trade == null) { - log.warn("Dispute's trade is null for trade {}", dispute.getTradeId()); + log.warn("Dispute's trade is null for trade {}, defaulting to dispute state text 'closed'", dispute.getTradeId()); return Res.get("support.closed"); } if (dispute.isClosed()) return Res.get("support.closed"); switch (trade.getDisputeState()) { + case NO_DISPUTE: + return Res.get("shared.pending"); case DISPUTE_REQUESTED: return Res.get("support.requested"); default: diff --git a/desktop/src/main/java/haveno/desktop/theme-dark.css b/desktop/src/main/java/haveno/desktop/theme-dark.css index db5c66a264..d045e3b5b0 100644 --- a/desktop/src/main/java/haveno/desktop/theme-dark.css +++ b/desktop/src/main/java/haveno/desktop/theme-dark.css @@ -241,6 +241,10 @@ -fx-border-width: 0 0 10 0; } +#address-text-field.jfx-text-field:readonly { + -fx-background-color: derive(-bs-background-color, 15%); +} + .wallet-seed-words { -fx-text-fill: -bs-color-gray-6; } @@ -553,3 +557,14 @@ -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; + -fx-background-radius: 3; + -fx-background-insets: 0, 1; +} + +.toggle-button-no-slider:selected { + -fx-background-color: -bs-color-gray-bbb; +} diff --git a/desktop/src/main/java/haveno/desktop/theme-light.css b/desktop/src/main/java/haveno/desktop/theme-light.css index 8f057a5054..7605eb1819 100644 --- a/desktop/src/main/java/haveno/desktop/theme-light.css +++ b/desktop/src/main/java/haveno/desktop/theme-light.css @@ -41,7 +41,7 @@ -bs-rd-green: #0b65da; -bs-rd-green-dark: #3EA34A; -bs-rd-nav-selected: #0b65da; - -bs-rd-nav-deselected: rgba(255, 255, 255, 0.59); + -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; @@ -125,3 +125,8 @@ .progress-bar > .secondary-bar { -fx-background-color: -bs-color-gray-3; } + +.toggle-button-no-slider { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} diff --git a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java index 2a38895421..8c725188c1 100644 --- a/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/haveno/desktop/util/DisplayUtils.java @@ -34,6 +34,7 @@ import java.util.Optional; @Slf4j public class DisplayUtils { private static final int SCALE = 3; + private static final String LOCKED = ".locked"; public static String formatDateTime(Date date) { return FormattingUtils.formatDateTime(date, true); @@ -116,18 +117,18 @@ public class DisplayUtils { // Offer direction /////////////////////////////////////////////////////////////////////////////////////////// - public static String getDirectionWithCode(OfferDirection direction, String currencyCode) { + public static String getDirectionWithCode(OfferDirection direction, String currencyCode, boolean isPrivate) { if (CurrencyUtil.isTraditionalCurrency(currencyCode)) - return (direction == OfferDirection.BUY) ? Res.get("shared.buyCurrency", Res.getBaseCurrencyCode()) : Res.get("shared.sellCurrency", Res.getBaseCurrencyCode()); + 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", currencyCode) : Res.get("shared.sellCurrency", currencyCode); + return (direction == OfferDirection.SELL) ? Res.get("shared.buyCurrency" + (isPrivate ? LOCKED : ""), currencyCode) : Res.get("shared.sellCurrency" + (isPrivate ? LOCKED : ""), currencyCode); } - public static String getDirectionBothSides(OfferDirection direction) { + public static String getDirectionBothSides(OfferDirection direction, boolean isPrivate) { String currencyCode = Res.getBaseCurrencyCode(); return direction == OfferDirection.BUY ? - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : - Res.get("formatter.makerTaker", currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); + Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.buyer"), currencyCode, Res.get("shared.seller")) : + Res.get("formatter.makerTaker" + (isPrivate ? LOCKED : ""), currencyCode, Res.get("shared.seller"), currencyCode, Res.get("shared.buyer")); } public static String getDirectionForBuyer(boolean isMyOffer, String currencyCode) { @@ -170,16 +171,16 @@ public class DisplayUtils { } } - public static String getOfferDirectionForCreateOffer(OfferDirection direction, String 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", Res.get("shared.buy"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.traditional", Res.get("shared.sell"), baseCurrencyCode); + 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", Res.get("shared.buy"), currencyCode, Res.get("shared.selling"), baseCurrencyCode) : - Res.get("formatter.youAreCreatingAnOffer.crypto", Res.get("shared.sell"), currencyCode, Res.get("shared.buying"), baseCurrencyCode); + 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); } } diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index 7df49216a2..ae3e7ed266 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -2340,8 +2340,8 @@ public class FormBuilder { return getRegularIconForLabel(icon, label, null); } - public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String style) { - return getIconForLabel(icon, "1.231em", label, style); + public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String styleClass) { + return getIconForLabel(icon, "1.231em", label, styleClass); } public static Text getIcon(GlyphIcons icon) { diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 2c314ea5d8..e825e29e6e 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -134,8 +134,6 @@ public class GUIUtil { private static Preferences preferences; - public static TradeCurrency TOP_CRYPTO = CurrencyUtil.getTradeCurrency("BTC").get(); - public static void setPreferences(Preferences preferences) { GUIUtil.preferences = preferences; } @@ -208,15 +206,17 @@ public class GUIUtil { persistenceManager.readPersisted(fileName, persisted -> { StringBuilder msg = new StringBuilder(); HashSet paymentAccounts = new HashSet<>(); - persisted.getList().forEach(paymentAccount -> { - String id = paymentAccount.getId(); - if (user.getPaymentAccount(id) == null) { - paymentAccounts.add(paymentAccount); - msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); - } else { - msg.append(Res.get("guiUtil.accountImport.noImport", id)); - } - }); + synchronized (persisted.getList()) { + persisted.getList().forEach(paymentAccount -> { + String id = paymentAccount.getId(); + if (user.getPaymentAccount(id) == null) { + paymentAccounts.add(paymentAccount); + msg.append(Res.get("guiUtil.accountExport.tradingAccount", id)); + } else { + msg.append(Res.get("guiUtil.accountImport.noImport", id)); + } + }); + } user.addImportedPaymentAccounts(paymentAccounts); new Popup().feedback(Res.get("guiUtil.accountImport.imported", path, msg)).show(); }, @@ -1033,12 +1033,4 @@ public class GUIUtil { columnConstraints2.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); } - - public static void updateTopCrypto(Preferences preferences) { - TradeCurrency tradeCurrency = preferences.getPreferredTradeCurrency(); - if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) { - return; - } - TOP_CRYPTO = tradeCurrency; - } } diff --git a/desktop/src/main/resources/images/lock.png b/desktop/src/main/resources/images/lock.png deleted file mode 100644 index 3a4bba5d91..0000000000 Binary files a/desktop/src/main/resources/images/lock.png and /dev/null differ diff --git a/desktop/src/main/resources/images/lock@2x.png b/desktop/src/main/resources/images/lock@2x.png new file mode 100644 index 0000000000..371f6aeb5d Binary files /dev/null and b/desktop/src/main/resources/images/lock@2x.png differ diff --git a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java index 19c2359245..971c15bff9 100644 --- a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -94,6 +94,7 @@ public class TradesChartsViewModelTest { 0, null, null, + null, null); @BeforeEach diff --git a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index 98c933ed4a..e9d39f47bf 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -53,7 +53,7 @@ public class CreateOfferDataModelTest { when(xmrWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(preferences.isUsePercentageBasedPrice()).thenReturn(true); - when(preferences.getBuyerSecurityDepositAsPercent(null)).thenReturn(0.01); + when(preferences.getSecurityDepositAsPercent(null)).thenReturn(0.01); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); @@ -88,7 +88,7 @@ public class CreateOfferDataModelTest { when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD")); + model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } @@ -109,7 +109,7 @@ public class CreateOfferDataModelTest { when(user.findFirstPaymentAccountWithCurrency(new TraditionalCurrency("USD"))).thenReturn(zelleAccount); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD")); + model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true); assertEquals("USD", model.getTradeCurrencyCode().get()); } } 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 4b33c8bab7..19756fc523 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 @@ -55,6 +55,7 @@ import static haveno.desktop.maker.PreferenceMakers.empty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -99,7 +100,7 @@ public class CreateOfferViewModelTest { when(paymentAccount.getPaymentMethod()).thenReturn(PaymentMethod.ZELLE); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); + when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any(), anyBoolean())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("ES", "Spain", null)); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); @@ -116,7 +117,7 @@ public class CreateOfferViewModelTest { coinFormatter, tradeStats, null); - dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero")); + dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero"), true); dataModel.activate(); model = new CreateOfferViewModel(dataModel, 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 bce59e6541..b7cc852ee1 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 @@ -241,7 +241,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForAmount.intValue()); } @@ -255,7 +255,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -273,7 +273,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -292,7 +292,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForVolume.intValue()); } @@ -306,7 +306,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -324,7 +324,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -342,7 +342,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForPrice.intValue()); } @@ -356,7 +356,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -374,7 +374,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @@ -409,7 +409,7 @@ public class OfferBookViewModelTest { item4.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item1, item2); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService, null, null, null, getPriceUtil(), null, coinFormatter, null); model.activate(); @@ -430,7 +430,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); - final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, + final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null, null, null, null, getPriceUtil(), null, coinFormatter, null); final OfferBookListItem item = make(xmrBuyItem.but( @@ -632,6 +632,7 @@ public class OfferBookViewModelTest { 0, null, null, + null, null)); } } diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index ae6f73ac52..2496dbdbbd 100644 --- a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java @@ -111,6 +111,7 @@ public class OfferMaker { lookup.valueOf(protocolVersion, 0), getLocalHostNodeWithPort(99999), null, + null, null)); public static final Maker xmrUsdOffer = a(Offer); diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 54bebbbf12..caa76ac1a9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Haveno -Thanks for wishing to help! Here there are some guidelines and information about the development process. We suggest you to join the [matrix](https://app.element.io/#/room/#haveno-dev:haveno.network) room `#haveno-dev` (relayed on [IRC/Libera](irc://irc.libera.chat/#haveno-dev)) and have a chat with the devs, so that we can help get you started. +Thanks for wishing to help! Here there are some guidelines and information about the development process. We suggest you to join the [matrix](https://app.element.io/#/room/#haveno-development:monero.social) room `#haveno-development` (relayed on [IRC/Libera](irc://irc.libera.chat/#haveno-development)) and have a chat with the devs, so that we can help get you started. Issues are tracked on GitHub. We use [a label system](https://github.com/haveno-dex/haveno/issues/50) and GitHub's [project boards](https://github.com/haveno-dex/haveno/projects) to simplify development. Make sure to take a look at those and to follow the priorities suggested. diff --git a/docs/bounties.md b/docs/bounties.md index e8de0828bb..ee52d69ed2 100644 --- a/docs/bounties.md +++ b/docs/bounties.md @@ -1,6 +1,6 @@ ## Bounties -We use bounties to incentivize development and reward contributors. All issues available for a bounty are listed [on the Kanban board](https://github.com/orgs/haveno-dex/projects/2). It's possible to list on each repository the issues with a bounty on them, by searching issues with the '💰bounty' label. +We use bounties to incentivize development and reward contributors. All issues available for a bounty are listed [here](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue%20state%3Aopen%20label%3A%F0%9F%92%B0bounty). It's possible to list on each repository the issues with a bounty on them, by searching issues with the '💰bounty' label. To receive a bounty, you agree to these conditions: diff --git a/docs/create-mainnet.md b/docs/create-mainnet.md index 38c211d4cd..4b220e162e 100644 --- a/docs/create-mainnet.md +++ b/docs/create-mainnet.md @@ -118,6 +118,10 @@ The price node is separated from Haveno and is run as a standalone service. To d 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). +### Update the download URL + +Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. + ## Review all local changes For comparison, placeholders to run on mainnet are marked [here on this branch](https://github.com/haveno-dex/haveno/tree/mainnet_placeholders). diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 2f7590d0ec..686c6cd615 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. @@ -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,6 +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`. @@ -266,7 +272,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master Set the mandatory minimum version for trading (optional) -If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.16) in the field labeled "Min. version required for trading". +If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.19) in the field labeled "Min. version required for trading". Send update alert diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 337d44c615..e09b953df7 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -8,7 +8,7 @@ This document is a guide for Haveno development. ## Run the UI proof of concept -Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-in-a-browser) to run Haveno's UI proof of concept in a browser. +Follow [instructions](https://github.com/haveno-dex/haveno-ui-poc) to run Haveno's UI proof of concept in a browser. This proof of concept demonstrates using Haveno's gRPC server with a web frontend (react and typescript) instead of Haveno's JFX application. @@ -28,11 +28,11 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run 2. Define the new service or message in Haveno's [protobuf definition](../proto/src/main/proto/grpc.proto). 3. Clean and build Haveno after modifying the protobuf definition: `make clean && make` 4. Implement the new service in Haveno's backend, following existing patterns.
    - For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L104) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreApi.java#L128) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreOffersService.java#L126) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/offer/OfferBookService.java#L193). + For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L102) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreApi.java#L403) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreOffersService.java#L131) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/offer/OfferBookService.java#L248). 5. Build Haveno: `make` 6. Update the gRPC client in haveno-ts: `npm install` -7. Add the corresponding typescript method(s) to [haveno.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.ts) with clear and concise documentation. -8. Add clean and comprehensive tests to [haveno.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/haveno.test.ts), following existing patterns. +7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation. +8. Add clean and comprehensive tests to [HavenoClient.test.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.test.ts), following existing patterns. 9. Run the tests with `npm run test -- -t 'my test'` to run tests by name and `npm test` to run all tests together. Ensure all tests pass and there are no exception stacktraces in the terminals of Alice, Bob, or the arbitrator. 10. Open pull requests to the haveno and haveno-ts projects for the backend and frontend implementations. diff --git a/docs/external-tor-usage.md b/docs/external-tor-usage.md new file mode 100644 index 0000000000..ace4a2817a --- /dev/null +++ b/docs/external-tor-usage.md @@ -0,0 +1,460 @@ +# **Using External `tor` with Haveno** +## [How to Install little-t-`tor` for Your Platform](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor) + +The following `tor` installation instructions are presented here for convenience. + +* **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer to the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** + +* **Notes:** + + For optimum compatibility with Haveno the running `tor` version should match that of the internal Haveno `tor` version + + For best results, use a version of `tor` which supports the [Onion Service Proof of Work](https://onionservices.torproject.org/technology/security/pow) (`PoW`) mechanism + * (IE: `GNU` build of `tor`) + +--- + +* **Note Regarding Admin Access:** + + To install `tor` you need root privileges. Below all commands that need to be run as `root` user like `apt` and `dpkg` are prepended with `#`, while commands to be run as user with `$` resembling the standard prompt in a terminal. + +### macOS +#### Install a Package Manager + Two of the most popular package managers for `macOS` are: + + [`Homebrew`](https://brew.sh) + + and + + [`Macports`](https://www.macports.org) + + (You can use the package manager of your choice) + + + Install [`Homebrew`](https://brew.sh) + + Follow the instructions on [brew.sh](https://brew.sh) + + + Install [`Macports`](https://www.macports.org) + + Follow the instructions on [macports.org](https://www.macports.org) + +#### Package Installation +##### [`Homebrew`](https://brew.sh) + ```shell + # brew update && brew install tor + ``` + +##### [`Macports`](https://www.macports.org) + ```shell + # port sync && port install tor + ``` + +### Debian / Ubuntu +* *Do **not** use the packages in Ubuntu's universe. In the past they have not reliably been updated. That means you could be missing stability and security fixes.* + +* Configure the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) + + Enable the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) + +#### Package Installation +```shell +# apt update && apt install tor +``` + +### Fedora + * Configure the [Official Tor Package Repository](https://rpm.torproject.org/fedora) + + Enable the [Official Tor Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) + +#### Package Installation +``` +# dnf update && dnf install tor +``` + +### Arch Linux +#### Package Installation +```shell +# pacman -Fy && pacman -Syu tor +``` + +### Installing `tor` from source +#### Download Latest Release & Dependencies +The latest release of `tor` can be found on the [download](https://www.torproject.org/download/tor) page + +* When building from source: + + *First* install `libevent`,`openssl` & `zlib` + + *(Including the -devel packages when applicable)* + +#### Install `tor` +```shell +$ tar -xzf tor-.tar.gz; cd tor- +``` + +* Replace \ with the latest version of `tor` + + > For example, `tor-0.4.8.14` + +```shell +$ ./configure && make +``` + +* Now you can run `tor` (0.4.3.x and Later) locally like this: + +```shell +$ ./src/app/tor +``` + +Or, you can run `make install` (as `root` if necessary) to install it globally into `/usr/local/` + +* Now you can run `tor` directly without absolute path like this: + +```shell +$ tor +``` + +### Windows +#### Download +* Download the `Windows Expert Bundle` from the [Official Tor Project's Download page](https://www.torproject.org/download/tor) + +#### Extract +* Extract Archive to Disk + +#### Open Terminal +* Open PowerShell with Admin Privileges + +#### Change to Location of Extracted Archive +* Navigate to `Tor` Directory + +#### Package Installation +* v10 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +* v11 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +#### Create Service +```powershell +PS C:\Tor\> sc create tor start=auto binPath="\Tor\tor.exe -nt-service" +``` + +#### Start Service +```powershell +PS C:\Tor\> sc start tor +``` + +## Configuring `tor` via `torrc` +#### [I'm supposed to "edit my torrc". What does that mean?](https://support.torproject.org/tbb/tbb-editing-torrc/) +* Per the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/): + * **WARNING:** Do **NOT** follow random advice instructing you to edit your torrc! Doing so can allow an attacker to compromise your security and anonymity through malicious configuration of your torrc. + + **Note:** + + The `torrc` location will ***not*** match those stated in the documentation linked above and will vary across each platform. + +#### [Sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) +Users are ***strongly*** encouraged to review both the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/) as well as the [sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) before proceeding. + +#### Enable `torControlPort` in `torrc` +In order for Haveno to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. + +#### [Authentication](https://spec.torproject.org/control-spec/implementation-notes.html#authentication) +Per the [Tor Control Protocol - Implementation Notes](https://spec.torproject.org/control-spec/implementation-notes.html): + + * ***"If the control port is open and no authentication operation is enabled, `tor` trusts any local user that connects to the control port. This is generally a poor idea."*** + +##### `CookieAuthentication` +If the `CookieAuthentication` option is true, `tor` writes a *"magic cookie"* file named `control_auth_cookie` into its data directory (or to another file specified in the `CookieAuthFile` option). + +##### Example: +```shell +ControlPort 9051 +CookieAuthentication 1 +``` + +##### `HashedControlPassword` +If the `HashedControlPassword` option is set, it must contain the salted hash of a secret password. The salted hash is computed according to the S2K algorithm in `RFC 2440` of `OpenPGP`, and prefixed with the s2k specifier. This is then encoded in hexadecimal, prefixed by the indicator sequence "16:". + +* `HashedControlPassword` can be generated like so: + ```shell + $ tor --hash-password + ``` + +###### Example: +```shell +ControlPort 9051 +HashedControlPassword 16:C01147DC5F4DA2346056668DD23522558D0E0C8B5CC88FE72EEBC51967 +``` + +##### Restart `tor` +`tor` must be restarted for changes to `torrc` to be applied. + +### \* ***Optional*** \* +#### [Set Up Your Onion Service](https://community.torproject.org/onion-services/setup) + +While not a *strict* requirement for use with Haveno, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) + + * ***Only Required When Using The Haveno `--hiddenServiceAddress` Option*** + +Please see the [Official Tor Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services + +--- + +## Haveno's `tor` Aware Options + +Haveno is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. + +While some are mutually exclusive, many are cross-applicable. + +Users are encouraged to experiment with options before use to determine which options best fit their personal threat profile. + +### Options +#### `--hiddenServiceAddress` +* Function: + + This option configures a *static* Hidden Service Address to listen on + +* Expected Input Format: + + `` + + (`ed25519`) + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--socks5ProxyXmrAddress` +* Function: + + A proxy address to be used for `monero` network + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcFile` +* Function: + + An existing `torrc`-file to be sourced for `tor` + + **Note:** + + `torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcOptions` +* Function: + + A list of `torrc`-entries to amend to Haveno's `torrc` + + **Note:** + + *`torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* + +* Expected Input Format: + + `` + +* Acceptable Values + + `<^([^\s,]+\s[^,]+,?\s*)+$>` + +* Default value: + + `null` + +#### `--torControlHost` ++ Function + + The control `hostname` or `IP` of an already running `tor` service to be used by Haveno + +* Expected Input Format + + `` + + (`hostname`, `IPv4` or `IPv6`) + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlPort` ++ Function + + The control port of an already running `tor` service to be used by Haveno + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `-1` + +#### `--torControlPassword` ++ Function + + The password for controlling the already running `tor` service + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlCookieFile` ++ Function + + The cookie file for authenticating against the already running `tor` service + * Used in conjunction with `--torControlUseSafeCookieAuth` option + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlUseSafeCookieAuth` ++ Function + + Use the `SafeCookie` method when authenticating to the already running `tor` service + +* Expected Input Format + + `null` + +* Acceptable Values + + `none` + +* Default Value + + `off` + +#### `--torStreamIsolation` ++ Function + + Use stream isolation for Tor + * This option is currently considered ***experimental*** + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `off` + +#### `--useTorForXmr` ++ Function + + Configure `tor` for `monero` connections with ***either***: + + * after_sync + + **or** + + * off + + **or** + + * on + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `AFTER_SYNC` + +#### `--socks5DiscoverMode` ++ Function + + Specify discovery mode for `monero` nodes + +* Expected Input Format + + `` + +* Acceptable Values + + `ADDR, DNS, ONION, ALL` + + One or more comma separated. + + *(Will be **OR**'d together)* + +* Default Value + + `ALL` + +--- + +## Starting Haveno Using Externally Available `tor` +### Dynamic Onion Assignment via `--torControlPort` +```shell +$ /opt/haveno/bin/Haveno --torControlPort='9051' --torControlCookieFile='/var/run/tor/control.authcookie' --torControlUseSafeCookieAuth --useTorForXmr='on' --socks5ProxyXmrAddress='127.0.0.1:9050' +``` + +### Static Onion Assignment via `--hiddenServiceAddress` +```shell +$ /opt/haveno/bin/Haveno --socks5ProxyXmrAddress='127.0.0.1:9050' --useTorForXmr='on' --hiddenServiceAddress='2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion' +``` diff --git a/docs/import-haveno.md b/docs/import-haveno.md index b05e8925a8..23f0a7e8c3 100644 --- a/docs/import-haveno.md +++ b/docs/import-haveno.md @@ -1,10 +1,16 @@ -## Importing Haveno into development environment +# Importing Haveno dev environment This document describes how to import Haveno into an integrated development environment (IDE). -## Importing Haveno into Eclipse IDE +First [install and run a Haveno test network](installing.md), then use the following instructions to import Haveno into an IDE. -These steps describe how to import Haveno into Eclipse IDE for development. You can also develop using [IntelliJ IDEA](#importing-haveno-into-intellij-idea) or VSCode if you prefer. +## Visual Studio Code (recommended) + +1. Download and open Visual Studio Code: https://code.visualstudio.com/. +2. File > Add folder to Workspace... +3. Browse to the `haveno` git project. + +## Eclipse IDE > Note: Use default values unless specified otherwise. @@ -26,7 +32,7 @@ These steps describe how to import Haveno into Eclipse IDE for development. You You are now ready to make, run, and test changes to the Haveno project! -## Importing Haveno into IntelliJ IDEA +## IntelliJ IDEA > Note: These instructions are outdated and for Haveno. diff --git a/docs/installing.md b/docs/installing.md index 3edb2f2330..eefd844ce6 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -1,13 +1,13 @@ # Build and run Haveno -These are the steps needed to build and run Haveno. You can test it locally or on our test network using the official Haveno repository. +These are the steps to build and run Haveno using the *official test network*. -> [!note] -> Trying to use Haveno on mainnet? +> [!warning] +> The official Haveno repository does not support making real trades directly. > -> The official Haveno repository does not operate or endorse any mainnet network. +> To make real trades with Haveno, first find a third party network, and then use their installer or build their repository. We do not endorse any networks at this time. > -> Find a third party network and use their installer or build their repository. Alternatively [create your own mainnet network](create-mainnet.md). +> Alternatively, you can [create your own mainnet network](create-mainnet.md). ## Install dependencies @@ -19,9 +19,9 @@ On Windows, first install MSYS2: 4. Update pacman: `pacman -Syy` 5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. - 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` + 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git zip unzip` - 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` + 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git zip unzip` On all platforms, install Java JDK 21: @@ -30,6 +30,8 @@ curl -s "https://get.sdkman.io" | bash sdk install java 21.0.2.fx-librca ``` +Restart the terminal for the changes to take effect. + ## Build Haveno If it's the first time you are building Haveno, run the following commands to download the repository, the needed dependencies, and build the latest release. If using a third party network, replace the repository URL with theirs: @@ -75,7 +77,7 @@ Steps: 1. Run `make user1-desktop-stagenet` to start the application. 2. Click on the "Funds" tab in the top menu and copy the generated XMR address. -3. Go to the [stagenet faucet](https://community.rino.io/faucet/stagenet/) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. +3. Go to the [stagenet faucet](https://stagenet-faucet.xmr-tw.org) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. 4. While you wait the 10 confirmations (20 minutes) needed for your funds to be spendable, create a fiat account by clicking on "Account" in the top menu, select the "National currency accounts" tab, then add a new account. For simplicity, we suggest to test using a Revolut account with a random ID. 5. Now pick up an existing offer or open a new one. Fund your trade and wait 10 blocks for your deposit to be unlocked. 6. Now if you are taking a trade you'll be asked to confirm you have sent the payment outside Haveno. Confirm in the app and wait for the confirmation of received payment from the other trader. diff --git a/docs/tor-upgrade.md b/docs/tor-upgrade.md index 990cc5d161..cf7548207f 100644 --- a/docs/tor-upgrade.md +++ b/docs/tor-upgrade.md @@ -10,7 +10,7 @@ As per the project's authors, `netlayer` is _"essentially a wrapper around the o easy use and convenient integration into Kotlin/Java projects"_. Similarly, `tor-binary` is _"[the] Tor binary packaged in a way that can be used for java projects"_. The project -unpacks the tor browser binaries to extract and repackage the tor binaries themselves. +unpacks the Tor Browser binaries to extract and repackage the tor binaries themselves. Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. @@ -22,8 +22,8 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. - Find out which tor version Haveno currently uses - Find out the current `netlayer` version (see `netlayerVersion` in `haveno/build.gradle`) - - Find that release on the project's [releases page][3] - - The release description says which tor version it includes + - Find that tag on the project's [Tags page][3] + - The tag description says which tor version it includes - Find out the latest available tor release - See the [official tor changelog][4] @@ -32,23 +32,24 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. During this update, you will need to keep track of: - - the new tor browser version + - the new Tor Browser version - the new tor binary version Create a PR for the `master` branch of [tor-binary][2] with the following changes: - - Decide which tor browser version contains the desired tor binary version - - The official tor browser releases are here: https://dist.torproject.org/torbrowser/ - - For the chosen tor browser version, get the list of SHA256 checksums and its signature - - For example, for tor browser 10.0.12: - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt.asc + - Decide which Tor Browser version contains the desired tor binary version + - The latest official Tor Browser releases are here: https://dist.torproject.org/torbrowser/ + - All official Tor Browser releases are here: https://archive.torproject.org/tor-package-archive/torbrowser/ + - For the chosen Tor Browser version, get the list of SHA256 checksums and its signature + - For example, for Tor Browser 14.0.7: + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt.asc - Verify the signature of the checksums list (see [instructions][5]) - Update the `tor-binary` checksums - For each file present in `tor-binary/tor-binary-resources/checksums`: - - Rename the file such that it reflects the new tor browser version, but preserves the naming scheme + - Rename the file such that it reflects the new Tor Browser version, but preserves the naming scheme - Update the contents of the file with the corresponding SHA256 checksum from the list - - Update `torbrowser.version` to the new tor browser version in: + - Update `torbrowser.version` to the new Tor Browser version in: - `tor-binary/build.xml` - `tor-binary/pom.xml` - Update `version` to the new tor binary version in: @@ -72,7 +73,7 @@ next. ### 3. Update `netlayer` -Create a PR for the `externaltor` branch of [netlayer][1] with the following changes: +Create a PR for the `master` branch of [netlayer][1] with the following changes: - In `netlayer/pom.xml`: - Update `tor-binary.version` to the `tor-binary` commit ID from above (e.g. `a4b868a`) @@ -82,13 +83,13 @@ Create a PR for the `externaltor` branch of [netlayer][1] with the following cha - `netlayer/tor.external/pom.xml` - `netlayer/tor.native/pom.xml` -Once the PR is merged, make a note of the commit ID in the `externaltor` branch (for example `32779ac`), as it will be +Once the PR is merged, make a note of the commit ID in the `master` branch (for example `32779ac`), as it will be needed next. Create a tag for the new artefact version, having the new tor binary version as description, for example: ``` -# Create tag locally for new netlayer release, on the externaltor branch +# Create tag locally for new netlayer release, on the master branch git tag -s 0.7.0 -m"tor 0.4.5.6" # Push it to netlayer repo @@ -105,8 +106,6 @@ Create a Haveno PR with the following changes: - See instructions in `haveno/gradle/witness/gradle-witness.gradle` - - ## Credits Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for their work on the original @@ -115,8 +114,8 @@ Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for thei -[1]: https://github.com/bisq-network/netlayer "netlayer" -[2]: https://github.com/bisq-network/tor-binary "tor-binary" -[3]: https://github.com/bisq-network/netlayer/releases "netlayer releases" +[1]: https://github.com/haveno-dex/netlayer "netlayer" +[2]: https://github.com/haveno-dex/tor-binary "tor-binary" +[3]: https://github.com/haveno-dex/netlayer/tags "netlayer Tags" [4]: https://gitweb.torproject.org/tor.git/plain/ChangeLog "tor changelog" [5]: https://support.torproject.org/tbb/how-to-verify-signature/ "verify tor signature" diff --git a/gpg_keys/woodser.asc b/gpg_keys/woodser.asc index 2dcc3f3a7a..4bbc755edc 100644 --- a/gpg_keys/woodser.asc +++ b/gpg_keys/woodser.asc @@ -1,5 +1,4 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -Comment: GPGTools - https://gpgtools.org mQINBFpYwMsBEACpSn/AxDOGCELE9lmYPfvBzgw2+1xS3TX7kYdlvVDQf+8eCgGz 8ZpBY3lXdga/yMZZBoDknGzjlyaiG/vi7NljMQmWd5eGyhyfkWpeDXYLbiB5HlKe @@ -25,29 +24,42 @@ zA6zmydMyNeUOYKjqnimQUuHBhxuUl5FlokoWaXnUavJvOjVfsoTcNxCcvMHnhFN R5TmNLOLPXrXwdU0V86nDmHstXl+E02SWFTgZ8Vxg318ZLpIw3rb65zUALTfZwpl 32XhIUhBBnN0zRl3scGW+oj6ks8WgErQ7o6dYdTu17AIggNdpHXO3XXVnW0mS6tz IeCvDkEQxegoL/B83B+9LI//U9sc5iSCQOEZQ1YLUdEkNSFgr7HU9GPllop52HUB -GffqGoz4F7MXl3g2ZrkCDQRaWMDLARAAxCZjAQT2bz3wqyz0vDMzwbtPRtHo3DY+ -r1n6CLxXDXu8u6DWGvRX3M2Z7jrbJe4e/cYDSDfNVj9E2YIVyD8pUbv9AUYh5VBq -hQU5C+3aeReO1js2iS1Xk6IAJ60aqp/JsrnRyOQfpAnGQaZlvqomdbbrzZaAaOXv -dgbHyBRj2eHZtSfYkhndfstpkE28etoZhNZP2h0e5DVLmfniwgMmMuZoiJNzEAGG -e9kAxdkvKgRp9HDrj6mGkHmbw6bam87DVrveNTPp662H7gLpIcUUJxzV7LttZDJa -k1/JxCQVbPoy0Frmp3TxXhmSJlV1vGVX8SFucaxrSS8ARhCSBrf+hGypbDGm+Tg5 -+oa1gdUSw24FODk7ut6LNwEgJ4n9ubs/8EP7/9rReiVLjJsW46ZueS1EjFTneZM1 -VyeAqBKqbwj21H9KxTghogCxpPHe4tqTr3J8eFjVYoNZDoFO3b00kjhXWOWicbCt -aT4SYUsRZP5WuBwgQu8W4AGgQpCFv6kJ37ctYfeSduDfGsMK0EJxpxutaDZC2940 -VfUA38LORFbwzPaNAGV8e7mViqEEmDE4g6fT0vyGodCsAM5EIbP/Q4u6ftNfE7Mf -mmp2CLnqHsfVLUvGbH8GbMLqoS1bajy8t4HEU0OZ7N12IQ1hnfnKHrLKpfGKXfl4 -1jkrL2gnuyUAEQEAAYkCNgQYAQoAIBYhBFL9fAGHfKloyXEY0FWhDdSK3uXvBQJa -WMDLAhsMAAoJEFWhDdSK3uXvf3wQAJyXitW8l+D2AaaszKmm4VXYkqf+azrVmRLp -nqUMvIaxhJTY4J2H5bT6JAAEU3/Dp6/ghYvqGbz25r94PUkDPKZ/23MvBMFab8bi -I//pT+jJwQFXKrXEIWhuBNFvqKhL8OxMi1kqys3E456quueohQzZbKyzTAYrEBQX -8/fNf/qaGuWIzcrdWqAO1OxnO/LBTZIh4Jrn1spBh3nW/U6k3LLSsXsPkBv9EIHx -R680R8cstT9cLaxUzqBhXX+iKPq8MqWXD5hZKKBCylWybdfhGc4FF+OszduWDP4n -VahNGD7pFX9hCMi6K5uIRj8bMtVahN7bBiwZMp3nQRAGCO5upqowMaGJv7A9zQ14 -lPKEEOf+3kQUj2XUw4juRmViU91hpIRy4Hf/4Wry3AhqICf9mMgkm/tI1ez+moWQ -RhopYZ4WTNbIhQrSUtaEOQHBcJFinKuR4SXxxmrFHpZ37It3SZZ5zJyZHrLypT9r -y0xrm7JWF++wQVofqvzTmVtIiwbYADuL/fDvyolo85rSeoDSdZVGnvY2tipMhr0+ -qBDrOi3tSaFzU+pmd0/hBmeNxS1ciYnxA6Ei+w0v79mbgKywngMTq+wQDynXrIHe -Np1oXqGvFU9bQ6BhDDKS54pPHm0ZlEg80+vealNXpXIVtjSM2PlRpsTlmqs3YcIa -mqKdaDoa -=bRX1 ------END PGP PUBLIC KEY BLOCK----- +GffqGoz4F7MXl3g2ZrQzd29vZHNlciA8MTMwNjg4NTkrd29vZHNlckB1c2Vycy5u +b3JlcGx5LmdpdGh1Yi5jb20+iQJRBBMBCAA7FiEEUv18AYd8qWjJcRjQVaEN1Ire +5e8FAmfBv40CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQVaEN1Ire +5e8bDBAAgET7qqMAhymtofo0NSemxLck1xEcZfco3inX4HVAV3J8HBnZPP2q19IP +F+Lj2GTRJZstRWNLwD2+7N3LgPOTGt0X+f6BsHLP0NMR87I7NpBoU+QEJv6fY1Ld +kZbgqfX0MPgHWHVN2qOsgZXQE4WKJECVpb8hJVNicfXb3Em+g5AtbI7ff4ycpRqz +ajSTTnvcn6meoN/LgGHjnFmYkV8CXVfgpcvUQJNqNHsrk6/iFPiWly9zb7G/4Vh7 +MqdjEZwEfGwgjA8Tzeh4Cks1fLM5KcZdMgRUmTSXZJxVdrq7ODwT9uRwCLJyncRx +wA1VrZHqEtiv+k3U9ef7ZngVlRdwogam5WJzyCioNCxBBzs4Z3dm/ZWwR/80YSa1 +DIGq//ybOaZqJ15wNAPzqdM1CwLg17w1sY//eKFFUQPZ7KmhG42/wWYG6ka9wgai +x4iPzO73weQQU/kxa4hjnU07zw+NJUxHfsNmqgJW+fRKmi50h6uz5WxRDigjkdGR +oe0HLipZ3cQjgLHaqR4Uw86yyWXQUYxZ+gmStUkrN3hgAX+JuXBxvKKlQQYUS3/j +JwAepRhi3mkFyoJveGUyfYXvTgYddIiCXBpdRIZSlWOabSYfdxFq+CBuAi16IhII +ulgsAXwKqUuX464zEFb+Ept5ESnApm8qDDXAzCBHlM6tJcOi3ey5Ag0EWljAywEQ +AMQmYwEE9m898Kss9LwzM8G7T0bR6Nw2Pq9Z+gi8Vw17vLug1hr0V9zNme462yXu +Hv3GA0g3zVY/RNmCFcg/KVG7/QFGIeVQaoUFOQvt2nkXjtY7NoktV5OiACetGqqf +ybK50cjkH6QJxkGmZb6qJnW2682WgGjl73YGx8gUY9nh2bUn2JIZ3X7LaZBNvHra +GYTWT9odHuQ1S5n54sIDJjLmaIiTcxABhnvZAMXZLyoEafRw64+phpB5m8Om2pvO +w1a73jUz6euth+4C6SHFFCcc1ey7bWQyWpNfycQkFWz6MtBa5qd08V4ZkiZVdbxl +V/EhbnGsa0kvAEYQkga3/oRsqWwxpvk4OfqGtYHVEsNuBTg5O7reizcBICeJ/bm7 +P/BD+//a0XolS4ybFuOmbnktRIxU53mTNVcngKgSqm8I9tR/SsU4IaIAsaTx3uLa +k69yfHhY1WKDWQ6BTt29NJI4V1jlonGwrWk+EmFLEWT+VrgcIELvFuABoEKQhb+p +Cd+3LWH3knbg3xrDCtBCcacbrWg2QtveNFX1AN/CzkRW8Mz2jQBlfHu5lYqhBJgx +OIOn09L8hqHQrADORCGz/0OLun7TXxOzH5pqdgi56h7H1S1Lxmx/BmzC6qEtW2o8 +vLeBxFNDmezddiENYZ35yh6yyqXxil35eNY5Ky9oJ7slABEBAAGJAjYEGAEKACAW +IQRS/XwBh3ypaMlxGNBVoQ3Uit7l7wUCWljAywIbDAAKCRBVoQ3Uit7l7398EACc +l4rVvJfg9gGmrMyppuFV2JKn/ms61ZkS6Z6lDLyGsYSU2OCdh+W0+iQABFN/w6ev +4IWL6hm89ua/eD1JAzymf9tzLwTBWm/G4iP/6U/oycEBVyq1xCFobgTRb6ioS/Ds +TItZKsrNxOOeqrrnqIUM2Wyss0wGKxAUF/P3zX/6mhrliM3K3VqgDtTsZzvywU2S +IeCa59bKQYd51v1OpNyy0rF7D5Ab/RCB8UevNEfHLLU/XC2sVM6gYV1/oij6vDKl +lw+YWSigQspVsm3X4RnOBRfjrM3blgz+J1WoTRg+6RV/YQjIuiubiEY/GzLVWoTe +2wYsGTKd50EQBgjubqaqMDGhib+wPc0NeJTyhBDn/t5EFI9l1MOI7kZlYlPdYaSE +cuB3/+Fq8twIaiAn/ZjIJJv7SNXs/pqFkEYaKWGeFkzWyIUK0lLWhDkBwXCRYpyr +keEl8cZqxR6Wd+yLd0mWecycmR6y8qU/a8tMa5uyVhfvsEFaH6r805lbSIsG2AA7 +i/3w78qJaPOa0nqA0nWVRp72NrYqTIa9PqgQ6zot7Umhc1PqZndP4QZnjcUtXImJ +8QOhIvsNL+/Zm4CssJ4DE6vsEA8p16yB3jadaF6hrxVPW0OgYQwykueKTx5tGZRI +PNPr3mpTV6VyFbY0jNj5UabE5ZqrN2HCGpqinWg6Gg== +=4SFl +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5bc11c0134..15194cfc52 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,107 +205,49 @@ - - - + + + - - - + + + - - - + + + - - - - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -878,9 +820,9 @@ - - - + + + diff --git a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java index f9c498f08e..79317494bd 100644 --- a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java +++ b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java @@ -95,7 +95,9 @@ public class Socks5ProxyProvider { String[] tokens = socks5ProxyAddress.split(":"); if (tokens.length == 2) { try { - return new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + Socks5Proxy proxy = new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + proxy.resolveAddrLocally(false); + return proxy; } catch (UnknownHostException e) { log.error(ExceptionUtils.getStackTrace(e)); } 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 117ed4a494..e7a810332e 100644 --- a/p2p/src/main/java/haveno/network/p2p/P2PService.java +++ b/p2p/src/main/java/haveno/network/p2p/P2PService.java @@ -109,6 +109,8 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Getter private static NodeAddress myNodeAddress; + @Getter + private boolean isShutDownStarted = false; /////////////////////////////////////////////////////////////////////////////////////////// @@ -192,6 +194,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private void doShutDown() { log.info("P2PService doShutDown started"); + isShutDownStarted = true; if (p2PDataStorage != null) { p2PDataStorage.shutDown(); diff --git a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java index 451d3e7e7f..e1e20225c4 100644 --- a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java +++ b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java @@ -48,12 +48,14 @@ public class MailboxMessageList extends PersistableList { @Override public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder() - .setMailboxMessageList(protobuf.MailboxMessageList.newBuilder() - .addAllMailboxItem(getList().stream() - .map(MailboxItem::toProtoMessage) - .collect(Collectors.toList()))) - .build(); + synchronized (getList()) { + return protobuf.PersistableEnvelope.newBuilder() + .setMailboxMessageList(protobuf.MailboxMessageList.newBuilder() + .addAllMailboxItem(getList().stream() + .map(MailboxItem::toProtoMessage) + .collect(Collectors.toList()))) + .build(); + } } public static MailboxMessageList fromProto(protobuf.MailboxMessageList proto, 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 1a7f1b84df..79df171470 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -175,9 +175,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { // throttle logs of reported invalid requests private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds private static long lastLoggedInvalidRequestReportTs = 0; - private static int numUnloggedInvalidRequestReports = 0; + private static int numThrottledInvalidRequestReports = 0; private static long lastLoggedWarningTs = 0; - private static int numUnloggedWarnings = 0; + private static int numThrottledWarnings = 0; + private static long lastLoggedInfoTs = 0; + private static int numThrottledInfos = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -620,7 +622,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReportTs > LOG_THROTTLE_INTERVAL_MS; // count the number of unlogged reports since last log entry - if (!logReport) numUnloggedInvalidRequestReports++; + if (!logReport) numThrottledInvalidRequestReports++; // handle report if (logReport) log.warn("We got reported the ruleViolation {} at connection with address={}, uid={}, errorMessage={}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid(), errorMessage); @@ -654,8 +656,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { if (logReport) { - if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports); - numUnloggedInvalidRequestReports = 0; + if (numThrottledInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numThrottledInvalidRequestReports); + numThrottledInvalidRequestReports = 0; lastLoggedInvalidRequestReportTs = System.currentTimeMillis(); } } @@ -676,7 +678,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this); } else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) { closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT; - throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); + throttleInfo("Shut down caused by exception " + e.getMessage() + " on connection=" + this); } else if (e instanceof EOFException) { closeConnectionReason = CloseConnectionReason.TERMINATED; throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this); @@ -937,14 +939,26 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { } private synchronized void throttleWarn(String msg) { - boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; - if (logWarning) { + boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS; + if (doLog) { log.warn(msg); - if (numUnloggedWarnings > 0) log.warn("We received {} other log warnings since the last log entry", numUnloggedWarnings); - numUnloggedWarnings = 0; + if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings); + numThrottledWarnings = 0; lastLoggedWarningTs = System.currentTimeMillis(); } else { - numUnloggedWarnings++; + numThrottledWarnings++; + } + } + + private synchronized void throttleInfo(String msg) { + 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); + numThrottledInfos = 0; + lastLoggedInfoTs = System.currentTimeMillis(); + } else { + numThrottledInfos++; } } } diff --git a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java index b4d7e9785c..8c99e1f986 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java +++ b/p2p/src/main/java/haveno/network/p2p/network/TorNetworkNodeNetlayer.java @@ -40,8 +40,8 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { private Tor tor; private final String torControlHost; private Timer shutDownTimeoutTimer; - private boolean shutDownInProgress; - private boolean shutDownComplete; + private boolean isShutDownStarted; + private boolean isShutDownComplete; public TorNetworkNodeNetlayer(int servicePort, NetworkProtoResolver networkProtoResolver, @@ -65,20 +65,20 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { @Override public void shutDown(@Nullable Runnable shutDownCompleteHandler) { log.info("TorNetworkNodeNetlayer shutdown started"); - if (shutDownComplete) { + if (isShutDownComplete) { log.info("TorNetworkNodeNetlayer shutdown already completed"); if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); return; } - if (shutDownInProgress) { - log.warn("Ignoring request to shut down because shut down is in progress"); + if (isShutDownStarted) { + log.warn("Ignoring request to shut down because shut down already started"); return; } - shutDownInProgress = true; + isShutDownStarted = true; shutDownTimeoutTimer = UserThread.runAfter(() -> { log.error("A timeout occurred at shutDown"); - shutDownComplete = true; + isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); executor.shutdownNow(); }, SHUT_DOWN_TIMEOUT); @@ -96,7 +96,7 @@ public class TorNetworkNodeNetlayer extends TorNetworkNode { log.error("Shutdown TorNetworkNodeNetlayer failed with exception", e); } finally { shutDownTimeoutTimer.stop(); - shutDownComplete = true; + isShutDownComplete = true; if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); } }); diff --git a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java index fca906ff1c..9320f978cd 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java @@ -277,9 +277,6 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public void onFailure(@NotNull Throwable throwable) { - log.warn("Broadcast to " + connection.getPeersNodeAddressOptional() + " failed. ", throwable); - numOfFailedBroadcasts.incrementAndGet(); - if (stopped.get()) { return; } @@ -356,7 +353,8 @@ public class BroadcastHandler implements PeerManager.Listener { try { future.cancel(true); } catch (Exception e) { - if (!networkNode.isShutDownStarted()) throw e; + if (networkNode.isShutDownStarted()) return; // ignore if shut down + throw e; } }); sendMessageFutures.clear(); 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 91ba2278c0..07ea9397a5 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 @@ -40,8 +40,10 @@ import java.util.concurrent.TimeUnit; class KeepAliveHandler implements MessageListener { private static final Logger log = LoggerFactory.getLogger(KeepAliveHandler.class); - private static final int DELAY_MS = 10_000; + private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds + private static long lastLoggedWarningTs = 0; + private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// @@ -147,9 +149,8 @@ class KeepAliveHandler implements MessageListener { cleanup(); listener.onComplete(); } else { - log.warn("Nonce not matching. That should never happen.\n\t" + - "We drop that message. nonce={} / requestNonce={}", - nonce, pong.getRequestNonce()); + throttleWarn("Nonce not matching. That should never happen.\n" + + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + pong.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped already. We ignore that onMessage call."); @@ -167,4 +168,16 @@ class KeepAliveHandler implements MessageListener { delayTimer = null; } } + + private synchronized void throttleWarn(String msg) { + 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); + numThrottledWarnings = 0; + lastLoggedWarningTs = System.currentTimeMillis(); + } else { + numThrottledWarnings++; + } + } } 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 1a929289a0..0b10307ccb 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 @@ -45,6 +45,9 @@ class PeerExchangeHandler implements MessageListener { // We want to keep timeout short here private static final long TIMEOUT = 90; private static final int DELAY_MS = 500; + private static final long LOG_THROTTLE_INTERVAL_MS = 60000; // throttle logging warnings to once every 60 seconds + private static long lastLoggedWarningTs = 0; + private static int numThrottledWarnings = 0; /////////////////////////////////////////////////////////////////////////////////////////// @@ -173,9 +176,8 @@ class PeerExchangeHandler implements MessageListener { cleanup(); listener.onComplete(); } else { - log.warn("Nonce not matching. That should never happen.\n\t" + - "We drop that message. nonce={} / requestNonce={}", - nonce, getPeersResponse.getRequestNonce()); + throttleWarn("Nonce not matching. That should never happen.\n" + + "\tWe drop that message. nonce=" + nonce + ", requestNonce=" + getPeersResponse.getRequestNonce() + ", peerNodeAddress=" + connection.getPeersNodeAddressOptional().orElseGet(null)); } } else { log.trace("We have stopped that handler already. We ignore that onMessage call."); @@ -216,4 +218,15 @@ class PeerExchangeHandler implements MessageListener { } } + private synchronized void throttleWarn(String msg) { + 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); + numThrottledWarnings = 0; + lastLoggedWarningTs = System.currentTimeMillis(); + } else { + numThrottledWarnings++; + } + } } 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 cb6ba81860..b9615b5bcb 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -319,7 +319,7 @@ service XmrConnections { } rpc StopCheckingConnection(StopCheckingConnectionRequest) returns (StopCheckingConnectionReply) { } - rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) { + rpc GetBestConnection(GetBestConnectionRequest) returns (GetBestConnectionReply) { } rpc SetAutoSwitch(SetAutoSwitchRequest) returns (SetAutoSwitchReply) { } @@ -400,9 +400,9 @@ message StopCheckingConnectionRequest {} message StopCheckingConnectionReply {} -message GetBestAvailableConnectionRequest {} +message GetBestConnectionRequest {} -message GetBestAvailableConnectionReply { +message GetBestConnectionReply { UrlConnection connection = 1; } @@ -521,10 +521,14 @@ message PostOfferRequest { double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double buyer_security_deposit_pct = 8; + double security_deposit_pct = 8; string trigger_price = 9; bool reserve_exact_amount = 10; string payment_account_id = 11; + bool is_private_offer = 12; + bool buyer_as_taker_without_deposit = 13; + string extra_info = 14; + string source_offer_id = 15; } message PostOfferReply { @@ -570,6 +574,9 @@ message OfferInfo { string arbitrator_signer = 29; string split_output_tx_hash = 30; uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; + bool is_private_offer = 32; + string challenge = 33; + string extra_info = 34; } message AvailabilityResultWithDescription { @@ -785,6 +792,7 @@ message TakeOfferRequest { string offer_id = 1; string payment_account_id = 2; uint64 amount = 3 [jstype = JS_STRING]; + string challenge = 4; } message TakeOfferReply { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 00b4473686..022031d08d 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 { @@ -250,6 +251,7 @@ message InitTradeRequest { string reserve_tx_hex = 18; string reserve_tx_key = 19; string payout_address = 20; + string challenge = 21; } message InitMultisigRequest { @@ -384,7 +386,7 @@ message DisputeOpenedMessage { NodeAddress sender_node_address = 2; string uid = 3; SupportType type = 4; - string updated_multisig_hex = 5; + string opener_updated_multisig_hex = 5; PaymentSentMessage payment_sent_message = 6; } @@ -650,12 +652,13 @@ message OfferPayload { int64 lower_close_price = 30; int64 upper_close_price = 31; bool is_private_offer = 32; - string hash_of_challenge = 33; + string challenge_hash = 33; map extra_data = 34; int32 protocol_version = 35; NodeAddress arbitrator_signer = 36; bytes arbitrator_signature = 37; repeated string reserve_tx_key_images = 38; + string extra_info = 39; } enum OfferDirection { @@ -874,13 +877,14 @@ message PaymentAccountPayload { PayByMailAccountPayload pay_by_mail_account_payload = 32; CapitualAccountPayload capitual_account_payload = 33; PayseraAccountPayload Paysera_account_payload = 34; - PaxumAccountPayload Paxum_account_payload = 35; + PaxumAccountPayload paxum_account_payload = 35; SwiftAccountPayload swift_account_payload = 36; CelPayAccountPayload cel_pay_account_payload = 37; MoneseAccountPayload monese_account_payload = 38; VerseAccountPayload verse_account_payload = 39; CashAtAtmAccountPayload cash_at_atm_account_payload = 40; PayPalAccountPayload paypal_account_payload = 41; + PaysafeAccountPayload paysafe_account_payload = 42; } } @@ -1071,6 +1075,7 @@ message MoneyBeamAccountPayload { message VenmoAccountPayload { string email_or_mobile_nr_or_username = 1; } + message PayPalAccountPayload { string email_or_mobile_nr_or_username = 1; string extra_info = 2; @@ -1232,6 +1237,10 @@ message SwiftAccountPayload { string intermediary_address = 16; } +message PaysafeAccountPayload { + string email = 1; +} + message PersistableEnvelope { oneof message { SequenceNumberMap sequence_number_map = 1; @@ -1412,6 +1421,9 @@ message OpenOffer { string reserve_tx_hash = 9; string reserve_tx_hex = 10; string reserve_tx_key = 11; + string challenge = 12; + bool deactivated_by_trigger = 13; + string group_id = 14; } message Tradable { @@ -1455,6 +1467,7 @@ message Trade { SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 24; SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25; SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 26; + BUYER_RECEIVED_PAYMENT_RECEIVED_MSG = 27; } enum Phase { @@ -1528,6 +1541,7 @@ message Trade { string counter_currency_extra_data = 26; string uid = 27; bool is_completed = 28; + string challenge = 29; } message BuyerAsMakerTrade { @@ -1557,8 +1571,8 @@ message ProcessModel { bytes payout_tx_signature = 4; bool use_savings_wallet = 5; int64 funds_needed_for_trade = 6; - string payment_sent_message_state = 7; - string payment_sent_message_state_arbitrator = 8; + string payment_sent_message_state_seller = 7 [deprecated = true]; + string payment_sent_message_state_arbitrator = 8 [deprecated = true]; bytes maker_signature = 9; TradePeer maker = 10; TradePeer taker = 11; @@ -1570,6 +1584,7 @@ message ProcessModel { int64 seller_payout_amount_from_mediation = 17; int64 trade_protocol_error_height = 18; string trade_fee_address = 19; + bool import_multisig_hex_scheduled = 20; } message TradePeer { @@ -1601,7 +1616,7 @@ message TradePeer { string made_multisig_hex = 31; string exchanged_multisig_hex = 32; string updated_multisig_hex = 33; - bool deposits_confirmed_message_acked = 34; + bool deposits_confirmed_message_acked = 34 [deprecated = true]; string deposit_tx_hash = 35; string deposit_tx_hex = 36; string deposit_tx_key = 37; @@ -1610,6 +1625,9 @@ message TradePeer { string unsigned_payout_tx_hex = 40; int64 payout_tx_fee = 41; int64 payout_amount = 42; + string deposits_confirmed_message_state = 43; + string payment_sent_message_state = 44; + string payment_received_message_state = 45; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1723,9 +1741,9 @@ message PreferencesPayload { string rpc_user = 43; string rpc_pw = 44; string take_offer_selected_payment_account_id = 45; - double buyer_security_deposit_as_percent = 46; + double security_deposit_as_percent = 46; int32 ignore_dust_threshold = 47; - double buyer_security_deposit_as_percent_for_crypto = 48; + double security_deposit_as_percent_for_crypto = 48; int32 block_notify_port = 49; int32 css_theme = 50; bool tac_accepted_v120 = 51; @@ -1742,6 +1760,9 @@ message PreferencesPayload { bool split_offer_output = 62; bool use_sound_for_notifications = 63; bool use_sound_for_notifications_initialized = 64; + string buy_screen_other_currency_code = 65; + string sell_screen_other_currency_code = 66; + bool show_private_offers = 67; } message AutoConfirmSettings { @@ -1881,6 +1902,7 @@ message PaymentAccountForm { CASH_APP = 16; PAYPAL = 17; VENMO = 18; + PAYSAFE = 19; } FormId id = 1; repeated PaymentAccountFormField fields = 2; diff --git a/scripts/install_tails/README.md b/scripts/install_tails/README.md index b718255925..05b6470fd8 100644 --- a/scripts/install_tails/README.md +++ b/scripts/install_tails/README.md @@ -1,21 +1,22 @@ # Install Haveno on Tails -Install Haveno on Tails by following these steps: +After you already have a [Tails USB](https://tails.net/install/linux/index.en.html#download): + +1. Enable [persistent storage](https://tails.net/doc/persistent_storage/index.en.html). +2. Set up [administration password](https://tails.net/doc/first_steps/welcome_screen/administration_password/). +3. Activate dotfiles in persistent storage settings. +4. Execute the following command in the terminal to download and execute the installation script. -1. Enable persistent storage dotfiles and admin password before starting Tails. -2. Execute a one-line installation command to download and install Haveno: - ``` - curl -x socks5h://127.0.0.1:9050 -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: ``` - curl -x socks5h://127.0.0.1:9050 -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/download/v1.0.12/haveno-linux-deb.zip" "FAA2 4D87 8B8D 36C9 0120 A897 CA02 DAC1 2DAE 2D0F" + 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 ``` - -3. Upon successful execution of the script (no errors), the Haveno release will be installed to persistent storage and can be launched via the desktop shortcut in the 'Other' section of the start menu. +5. Start Haveno by finding the icon in the launcher under **Applications > Other**. > [!note] > If you have already installed Haveno on Tails, we recommend moving your data directory (/home/amnesia/Persistent/Haveno-example) to the new default location (/home/amnesia/Persistent/haveno/Data/Haveno-example), to retain your history and for future support. \ No newline at end of file diff --git a/scripts/install_tails/haveno-install.sh b/scripts/install_tails/haveno-install.sh old mode 100644 new mode 100755 index 3f332cf21b..16529f35e0 --- a/scripts/install_tails/haveno-install.sh +++ b/scripts/install_tails/haveno-install.sh @@ -38,29 +38,21 @@ install_dir="${persistence_dir}/haveno/Install" dotfiles_dir="/live/persistence/TailsData_unlocked/dotfiles" persistent_desktop_dir="$dotfiles_dir/.local/share/applications" local_desktop_dir="/home/amnesia/.local/share/applications" - - -# Install dependencies -echo_blue "Installing dependencies ..." -sudo apt update && sudo apt install -y curl unzip - - -# Remove stale resources -rm -rf "${assets_dir}" +wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" # Create temp location for downloads echo_blue "Creating temporary directory for Haveno resources ..." -mkdir "${assets_dir}" || { echo_red "Failed to create directory ${assets_dir}"; exit 1; } +mkdir -p "${assets_dir}" || { echo_red "Failed to create directory ${assets_dir}"; exit 1; } # Download resources echo_blue "Downloading resources for Haveno on Tails ..." -curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/exec.sh https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/exec.sh || { echo_red "Failed to download resource (exec.sh)."; exit 1; } -curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/install.sh https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/install.sh || { echo_red "Failed to download resource (install.sh)."; exit 1; } -curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/haveno.desktop https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.desktop || { echo_red "Failed to resource (haveno.desktop)."; exit 1; } -curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/icon.png https://raw.githubusercontent.com/haveno-dex/haveno/master/scripts/install_tails/assets/icon.png || { echo_red "Failed to download resource (icon.png)."; exit 1; } -curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/haveno.yml https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.yml || { echo_red "Failed to download resource (haveno.yml)."; exit 1; } +wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/exec.sh || { echo_red "Failed to download resource (exec.sh)."; exit 1; } +wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/install.sh || { echo_red "Failed to download resource (install.sh)."; exit 1; } +wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.desktop || { echo_red "Failed to resource (haveno.desktop)."; exit 1; } +wget "${wget_flags}" -cqP "${assets_dir}" https://raw.githubusercontent.com/haveno-dex/haveno/master/scripts/install_tails/assets/icon.png || { echo_red "Failed to download resource (icon.png)."; exit 1; } +wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.yml || { echo_red "Failed to download resource (haveno.yml)."; exit 1; } # Create persistent directory @@ -92,17 +84,17 @@ fi # Download Haveno binary echo_blue "Downloading Haveno from URL provided ..." -curl --retry 10 --retry-delay 5 -L -o "${binary_filename}" "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } +wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } # Download Haveno signature file echo_blue "Downloading Haveno signature ..." -curl --retry 10 --retry-delay 5 -L -o "${signature_filename}" "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } +wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } # Download the GPG key echo_blue "Downloading signing GPG key ..." -curl --retry 10 --retry-delay 5 -L -o "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } +wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } # Import the GPG key @@ -132,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 unzip "${binary_filename}" && mv haveno*.deb "${package_filename}" + else mv -f "${binary_filename}" "${package_filename}" fi echo_blue "Haveno binaries have been successfully verified." @@ -144,9 +136,13 @@ 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}" +# Remove stale resources +rm -rf "${assets_dir}" + + # Completed confirmation echo_blue "Haveno installation setup completed successfully." diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md new file mode 100644 index 0000000000..c56b35cacd --- /dev/null +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -0,0 +1,401 @@ +# Haveno on Qubes/Whonix + +## **Conventions:** + ++ \# – Requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command + ++ $ or % – Requires given linux commands to be executed as a regular non-privileged user + ++ \ – Used to indicate user supplied variable + +--- + +## **Installation - Scripted & Manual (GUI + CLI):** +### *Acquire release files:* +#### In `dispXXXX` AppVM: +##### Clone repository +```shell +% git clone --depth=1 https://github.com/haveno-dex/haveno +``` + +--- + +### **Create TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dispXXXX` AppVM: +###### Prepare files for transfer to `dom0` +```shell +% tar -C haveno/scripts/install_qubes/scripts/0-dom0 -zcvf /tmp/haveno.tgz . +``` + +##### In `dom0`: +###### Copy files to `dom0` +```shell +$ mkdir -p /tmp/haveno && qvm-run -p dispXXXX 'cat /tmp/haveno.tgz' > /tmp/haveno.tgz && tar -C /tmp/haveno -zxfv /tmp/haveno.tgz +$ bash /tmp/haveno/0.0-dom0.sh && bash /tmp/haveno/0.1-dom0.sh && bash /tmp/haveno/0.2-dom0.sh +``` + +#### GUI +##### TemplateVM +###### Via `Qubes Manager`: + ++ Locate & highlight whonix-workstation-17 (TemplateVM) + ++ Right-Click "whonix-workstation-17" and select "Clone qube" from Drop-Down + ++ Enter "haveno-template" in "Name" + ++ Click OK Button + +##### NetVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "sys-haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "whonix-gateway-17" from "Template" Drop-Down + ++ Select "sys-firewall" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "512" for "Initial memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Enter "512" for "Max memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Tick "Provides network" Radio-Box + ++ Click "Apply" Button + ++ Click "OK" Button + +##### AppVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "haveno-template" from "Template" Drop-Down + ++ Select "sys-haveno" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "2048" for "Initial memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Enter "4096" for "Max memory" + +

    (Within reason, can adjust to personal preference)

    + ++ Click "Apply" Button + ++ Click "OK" Button + + +#### CLI +##### TemplateVM +###### In `dom0`: +```shell +$ qvm-clone whonix-workstation-17 haveno-template +``` + +##### NetVM +##### In `dom0`: +```shell +$ qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True +``` + +#### AppVM +##### In `dom0`: +```shell +$ qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +$ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno +``` + +--- + +### **Build TemplateVM, NetVM & AppVM:** +#### *TemplateVM Using Precompiled Package via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% qvm-copy haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh +``` + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% 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" +``` + +#### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* +##### In `haveno-template` TemplateVM: +###### Download & Import Project PGP Key +

    For Whonix On Qubes OS:

    + +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    Example:

    + +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    For Whonix On Anything Other Than Qubes OS:

    + +```shell +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

    Example:

    + +```shell +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + + +###### Download Release Files +

    For Whonix On Qubes OS:

    + +```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 +``` + +

    Note:

    +

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

    + +

    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 +``` + +

    Note:

    +

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

    + +###### Verify Release Files +```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 +``` + +###### 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 +``` + +#### *TemplateVM Building From Source via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "" "" "" +``` + +

    Example:

    + +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` ++ Upon Successful Compilation & Packaging, A `Filecopy` Confirmation Will Be Presented + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% sudo apt install -y ./QubesIncoming/dispXXXX/haveno.deb +``` + +#### *NetVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh +``` + ++ Select "sys-haveno" for "Target" Within Pop-Up + ++ Click "OK" Button + +##### In `sys-haveno` NetVM: +(Allow bootstrap process to complete) +```shell +% sudo zsh QubesIncoming/dispXXXX/2.0-haveno-netvm.sh +``` + +#### *NetVM (CLI)* +##### In `sys-haveno` NetVM: +###### Add `onion-grater` Profile +```shell +# onion-grater-add 40_haveno +``` + +###### Restart `onion-grater` Service +```shell +# systemctl restart onion-grater.service +# poweroff +``` + +#### *AppVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh +``` + ++ Select "haveno" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno` AppVM: +```shell +% sudo zsh QubesIncoming/dispXXXX/3.0-haveno-appvm.sh +``` + +#### *AppVM (CLI)* +##### In `haveno` AppVM: +###### Adjust `sdwdate` Configuration +```shell +# mkdir /usr/local/etc/sdwdate-gui.d +# printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +# systemctl restart sdwdate +``` + +###### Prepare Firewall Settings via `/rw/config/rc.local` +```shell +# printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +# printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +# printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local +``` + +###### View & Verify Change +```shell +# tail /rw/config/rc.local +``` + +

    Confirm output contains:

    + +> # Poke FW +> printf "EXTERNAL_OPEN_PORTS+=\" 9999 \"\n" | tee /usr/local/etc/whonix_firewall.d/50_user.conf +> +> # Restart FW +> whonix_firewall + +###### Restart `whonix_firewall` +```shell +# whonix_firewall +``` + +###### Create `haveno-Haveno.desktop` +```shell +# mkdir -p /home/$(ls /home)/\.local/share/applications +# 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 +``` + +###### View & Verify Change +```shell +# tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +``` + +

    Confirm output contains:

    + +> [Desktop Entry] +> Name=Haveno +> Comment=Haveno +> Exec=/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on +> Icon=/opt/haveno/lib/Haveno.png +> Terminal=false +> Type=Application +> Categories=Network +> MimeType= + +###### Poweroff +```shell +# poweroff +``` + +### **Remove TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dom0`: +```shell +$ bash /tmp/haveno/0.3-dom0.sh +``` + +#### GUI +##### Via `Qubes Manager`: + ++ Highlight "haveno" (AppVM) + ++ Click "Delete qube" + ++ Enter "haveno" + ++ Click "OK" Button + ++ Highlight "haveno-template" (TemplateVM) + ++ Click "Delete qube" + ++ Enter "haveno-template" + ++ Click "OK" Button + ++ Highlight "sys-haveno" (NetVM) + ++ Click "Delete qube" + ++ Enter "sys-haveno" + ++ Click "OK" Button + +#### CLI +##### In `dom0`: +```shell +$ qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno +``` diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md new file mode 100644 index 0000000000..72670e41ca --- /dev/null +++ b/scripts/install_whonix_qubes/README.md @@ -0,0 +1,75 @@ +# Install Haveno on Qubes/Whonix + + +After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed: + +1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/scripts/install_whonix_qubes/scripts). +2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.). +3. Consecutively execute the following commands in their respective destinations. + +--- + +## **Create VMs** +[`Qubes`](https://www.qubes-os.org/downloads) +### **In `dom0`:** + +```shell +$ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh +``` + +[`Whonix`](https://www.whonix.org/wiki/Download) On Anything Other Than [`Qubes`](https://www.qubes-os.org/downloads) + +- Clone `Whonix Workstation` To VM Named `haveno-template` +- Clone `Whonix Gateway` To VM Named `sys-haveno` +- Create New Linked VM Clone Based On `haveno-template` Named `haveno` + + +## **Build TemplateVM** +### *Via Binary Archive* +#### **In `haveno-template` `TemplateVM`:** + +```shell +% 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" +``` + +### *Via Source* +#### **In `dispXXXX` `AppVM`:** +```shell +% bash 1.0-haveno-templatevm.sh "" "" "" +``` + +

    Example:

    + +```shell +% bash 1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` + +#### **In `haveno-template` `TemplateVM`:** + +```shell +% sudo apt install -y haveno.deb +``` + +## **Build NetVM** +### **In `sys-haveno` `NetVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +## **Build AppVM** +### **In `haveno` `AppVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +--- + +Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md). diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh new file mode 100644 index 0000000000..5618cf1e12 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.0-dom0.sh + +## Create Haveno TemplateVM: +qvm-clone whonix-workstation-17 haveno-template + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh new file mode 100644 index 0000000000..befa8b6702 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.1-dom0.sh + +## Create Haveno NetVM: +qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh new file mode 100644 index 0000000000..6f52637632 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.2-dom0.sh + +## Create Haveno AppVM: +qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist - haveno + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh new file mode 100644 index 0000000000..4bdae35533 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.3-dom0.sh + +## Remove Haveno GuestVMs +qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno + 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 new file mode 100644 index 0000000000..f1ab43ae1b --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh @@ -0,0 +1,185 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/1.1-haveno-templatevm_maker.sh + + +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" + exit 1 + fi + ## Update & Upgrade + apt update && apt upgrade -y + + + ## Install wget + apt install -y wget + + + ## Function to print messages in blue: + echo_blue() { + echo -e "\033[1;34m$1\033[0m" + } + + + # Function to print error messages in red: + echo_red() { + echo -e "\033[0;31m$1\033[0m" + } + + + ## Sweep for old release files + rm *.asc desktop-*-SNAPSHOT-all.jar.SHA-256 haveno* + + + ## Define URL & PGP Fingerprint etc. vars: + user_url=$PRECOMPILED_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" + key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc + wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" + + + ## Debug: + 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" + + + ## Configure for tinyproxy: + export https_proxy=http://127.0.0.1:8082 + + + ## Download Haveno binary: + echo_blue "Downloading Haveno from URL provided ..." + wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } + + + ## Download Haveno signature file: + echo_blue "Downloading Haveno signature ..." + wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } + + + ## Download the GPG key: + echo_blue "Downloading signing GPG key ..." + wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } + + + ## Import the GPG key: + echo_blue "Importing the GPG key ..." + gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; } + + + ## Extract imported fingerprints: + imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n') + + + ## Remove spaces from the expected fingerprint for comparison: + formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ') + + + ## Check if the expected fingerprint is in the list of imported fingerprints: + if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then + echo_red "The imported GPG key fingerprint does not match the expected fingerprint." + exit 1 + fi + + + ## 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}"; + else echo_red "Verification failed!" && sleep 5 + exit 1; + fi + + + echo_blue "Haveno binaries have been successfully verified." + + + # Install Haveno: + echo_blue "Installing Haveno ..." + apt install -y ./"${package_filename}" || { echo_red "Failed to install Haveno."; exit 1; } + + ## Finalize + echo_blue "Haveno TemplateVM installation and configuration complete." + echo_blue "\nHappy Trading\!\n" + printf "%s \n" "Press [ENTER] to complete ..." + read ans + #exit + poweroff +} + + +function build { + if [[ -z $JAVA_URL || -z $JAVA_SHA1 || -z $SOURCE_URL ]]; then + printf "\nNo arguments provided!\n\nThis script requires three argument to be provided:\n\nURL for Java 21 JDK Debian Package\n\nSHA1 Hash for Java 21 JDK Debian Package\n\nURL for Remote Git Source Repository\n\nPlease review documentation and try again.\n\nExiting now ...\n" + exit 1 + fi + # Dependancies + sudo apt install -y make git expect fakeroot binutils + + # Java + curl -fsSLo jdk21.deb ${JAVA_URL} + if [[ $(shasum ./jdk21.deb | awk '{ print $1 }') == ${JAVA_SHA1} ]] ; then printf $'SHA Hash IS valid!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi + sudo apt install -y ./jdk21.deb + + # Build + git clone --depth=1 $SOURCE_URL + GIT_DIR=$(awk -F'/' '{ print $NF }' <<< "$SOURCE_URL") + cd ${GIT_DIR} + git checkout master + sed -i 's|XMR_STAGENET|XMR_MAINNET|g' desktop/package/package.gradle + ./gradlew clean build --refresh-keys --refresh-dependencies + + # Package + # Expect + cat <> /tmp/haveno_package_deb.exp +set send_slow {1 .1} +proc send {ignore arg} { + sleep 1.1 + exp_send -s -- \$arg +} +set timeout -1 +spawn ./gradlew packageInstallers --console=plain +match_max 100000 +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "app-image" +send -- \x03 +expect eof +DONE + + # Package + expect -f /tmp/haveno_package_deb.exp && find ./ -name '*.deb' -exec qvm-copy {} \; + printf "\nHappy Trading!\n" + +} + +if ! [[ $# -eq 2 || $# -eq 3 ]] ; then + printf "\nFor this script to function, user supplied arguments are required.\n\n" + printf "\nPlease review documentation and try again.\n\n" +fi + +if [[ $# -eq 2 ]] ; then + PRECOMPILED_URL=$1 + FINGERPRINT=$2 + remote +fi + +if [[ $# -eq 3 ]] ; then + JAVA_URL=$1 + JAVA_SHA1=$2 + SOURCE_URL=$3 + build +fi diff --git a/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh new file mode 100644 index 0000000000..d29e61dcf5 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh @@ -0,0 +1,30 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/2.0-haveno-netvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## onion-grater +# Add onion-grater Profile +echo_blue "\nAdding onion-grater Profile ..." +onion-grater-add 40_haveno + + +# Restart onion-grater +echo_blue "\nRestarting onion-grater Service ..." +systemctl restart onion-grater.service +echo_blue "Haveno NetVM configuration complete." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff + 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 new file mode 100644 index 0000000000..11582a8314 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh @@ -0,0 +1,61 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## Adjust sdwdate Configuration +mkdir -p /usr/local/etc/sdwdate-gui.d +printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +systemctl restart sdwdate + + +## Prepare Firewall Settings +echo_blue "\nConfiguring FW ..." +printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /rw/config/rc.local +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + + +## Restart FW +echo_blue "\nRestarting Whonix FW ..." +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 +chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + +echo_blue "Haveno AppVM configuration complete." +echo_blue "Refresh applications via Qubes Manager GUI now." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 51a85bbc4d..b846dff4c9 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.12"; + private static final String VERSION = "1.1.1"; private SeedNode seedNode; private Timer checkConnectionLossTime;